mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-08 03:18:36 +03:00
Clean master commit
This commit is contained in:
3
.babelrc
Normal file
3
.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||
}
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
build/
|
||||
node_modules/
|
||||
config.js
|
||||
17
config.example.js
Normal file
17
config.example.js
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
title: 'EXAMPLE_TITLE',
|
||||
SERVER_ROOT: 'http://localhost:4040',
|
||||
EVENT_EXT: '/api/<ORIGIN_NAME>/MAP2D_dev/rows',
|
||||
CATEGORY_EXT: '/api/<ORIGIN_NAME>/MAP2D_dev_category/rows',
|
||||
EVENT_DESC_ROOT: '/api/<ORIGIN_NAME>/MAP2D_dev/ids',
|
||||
TAG_TREE_EXT: '/api/<ORIGIN_NAME>/MAP2D_dev_tags/tree',
|
||||
SITES_EXT: '/api/<ORIGIN_NAME>/MAP2D_dev_sites/rows',
|
||||
MAP_ANCHOR: [27.5813121, -18.5161798],
|
||||
INCOMING_DATETIME_FORMAT: '%m/%d/%YT%H:%M',
|
||||
MAPBOX_TOKEN: 'SOME_MAPBOX_TOKEN',
|
||||
features: {
|
||||
USE_TAGS: false,
|
||||
USE_SEARCH: false,
|
||||
USE_SITES: false
|
||||
}
|
||||
}
|
||||
16
index.html
Normal file
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>TimeMap - Forensic Architecture</title>
|
||||
<link rel="stylesheet" href="https://api.mapbox.com/mapbox.js/v3.1.1/mapbox.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js" charset="utf-8"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="page">
|
||||
<div id="explore-app"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
48
package.json
Normal file
48
package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "guerrero",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"homepage": "",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --content-base static --mode development",
|
||||
"build": "NODE_ENV=production webpack --mode production"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"d3": "^4.9.1",
|
||||
"es6-promise": "^4.1.1",
|
||||
"joi": "^14.0.1",
|
||||
"leaflet": "^1.0.3",
|
||||
"leaflet-polylinedecorator": "^1.3.2",
|
||||
"normalizr": "^3.2.3",
|
||||
"object-hash": "^1.3.0",
|
||||
"react": "^15.5.4",
|
||||
"react-dom": "^15.5.4",
|
||||
"react-redux": "^5.0.4",
|
||||
"react-tabs": "^1.0.0",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^3.0.1",
|
||||
"uuid": "^3.1.0",
|
||||
"video.js": "^5.19.2",
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
"css-loader": "^1.0.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.4.4",
|
||||
"node-sass": "^4.9.4",
|
||||
"redux-devtools": "^3.4.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack": "^4.20.2",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.9"
|
||||
}
|
||||
}
|
||||
242
src/actions/index.js
Normal file
242
src/actions/index.js
Normal file
@@ -0,0 +1,242 @@
|
||||
// TODO: relegate these URLs entirely to environment variables
|
||||
const EVENT_DATA_URL = `${process.env.SERVER_ROOT}${process.env.EVENT_EXT}`;
|
||||
const CATEGORY_URL = `${process.env.SERVER_ROOT}${process.env.CATEGORY_EXT}`;
|
||||
const TAG_TREE_URL = `${process.env.SERVER_ROOT}${process.env.TAG_TREE_EXT}`;
|
||||
const SITES_URL = `${process.env.SERVER_ROOT}${process.env.SITES_EXT}`;
|
||||
const eventUrlMap = (event) => `${process.env.SERVER_ROOT}${process.env.EVENT_DESC_ROOT}/${(event.id) ? event.id : event}`
|
||||
|
||||
/*
|
||||
* Create an error notification object
|
||||
* Types: ['error', 'warning', 'good', 'neural']
|
||||
*/
|
||||
function makeError(type, id, message) {
|
||||
return {
|
||||
type: 'error',
|
||||
id,
|
||||
message: `${type} ${id}: ${message}`
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchDomain() {
|
||||
let events = [];
|
||||
let categories = [];
|
||||
let sites = [];
|
||||
let notifications = [];
|
||||
let tags = {};
|
||||
|
||||
function makeError(domainType) {
|
||||
notifications.push({
|
||||
message: `Something went wrong fetching ${domainType}. Check the URL or try disabling them in the config file.`,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
return dispatch => {
|
||||
dispatch(toggleFetchingDomain());
|
||||
const promises = [];
|
||||
|
||||
const eventPromise = fetch(EVENT_DATA_URL)
|
||||
.then(response => response.json())
|
||||
.then(jsonEv => { events = jsonEv; })
|
||||
.catch(err => { makeError('events')});
|
||||
promises.push(eventPromise);
|
||||
|
||||
const catPromise = fetch(CATEGORY_URL)
|
||||
.then(response => response.json())
|
||||
.then(jsonCat => { categories = jsonCat; })
|
||||
.catch(err => { makeError('categories')});
|
||||
promises.push(catPromise);
|
||||
|
||||
if (process.env.features.USE_SITES) {
|
||||
const sitesPromise = fetch(SITES_URL)
|
||||
.then(response => response.json())
|
||||
.then(jsonSites => { sites = jsonSites; })
|
||||
.catch(err => { makeError('sites')});
|
||||
promises.push(sitesPromise);
|
||||
}
|
||||
|
||||
if (process.env.features.USE_TAGS) {
|
||||
const tagTreePromise = fetch(TAG_TREE_URL)
|
||||
.then(response => response.json())
|
||||
.then(jsonTagTree => { tags = jsonTagTree; })
|
||||
.catch(err => { makeError('tags')});
|
||||
promises.push(tagTreePromise);
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(reponse => {
|
||||
dispatch(toggleFetchingDomain());
|
||||
return { events, categories, sites, tags, notifications };
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(fetchError(err.message))
|
||||
dispatch(toggleFetchingDomain());
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
export const FETCH_ERROR = 'FETCH_ERROR';
|
||||
export function fetchError(message) {
|
||||
return {
|
||||
type: FETCH_ERROR,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
export const UPDATE_DOMAIN = 'UPDATE_DOMAIN';
|
||||
export function updateDomain(domain) {
|
||||
return {
|
||||
type: UPDATE_DOMAIN,
|
||||
domain: {
|
||||
events: domain.events,
|
||||
categories: domain.categories,
|
||||
tags: domain.tags,
|
||||
sites: domain.sites,
|
||||
notifications: domain.notifications
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchEvents(events) {
|
||||
return dispatch => {
|
||||
dispatch(toggleFetchingEvents());
|
||||
const urls = events.map(eventUrlMap);
|
||||
return Promise.all(urls.map(url => fetch(url)
|
||||
.then(response => response.json())
|
||||
)
|
||||
)
|
||||
.then(json => {
|
||||
dispatch(toggleFetchingEvents());
|
||||
return json;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const UPDATE_HIGHLIGHTED = 'UPDATE_HIGHLIGHTED';
|
||||
export function updateHighlighted(highlighted) {
|
||||
return {
|
||||
type: UPDATE_HIGHLIGHTED,
|
||||
highlighted: highlighted
|
||||
};
|
||||
}
|
||||
|
||||
export const UPDATE_SELECTED = 'UPDATE_SELECTED';
|
||||
export function updateSelected(selected) {
|
||||
return {
|
||||
type: UPDATE_SELECTED,
|
||||
selected: selected
|
||||
};
|
||||
}
|
||||
|
||||
export const UPDATE_DISTRICT = 'UPDATE_DISTRICT';
|
||||
export function updateDistrict(district) {
|
||||
return {
|
||||
type: UPDATE_DISTRICT,
|
||||
district
|
||||
};
|
||||
}
|
||||
|
||||
export const UPDATE_FILTERS = 'UPDATE_FILTERS';
|
||||
export function updateFilters(filters) {
|
||||
return {
|
||||
type: UPDATE_FILTERS,
|
||||
filters: filters
|
||||
};
|
||||
}
|
||||
|
||||
export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE';
|
||||
export function updateTimeRange(range) {
|
||||
return {
|
||||
type: UPDATE_TIMERANGE,
|
||||
range
|
||||
};
|
||||
}
|
||||
|
||||
export const RESET_ALLFILTERS = 'RESET_ALLFILTERS';
|
||||
export function resetAllFilters() {
|
||||
return {
|
||||
type: RESET_ALLFILTERS
|
||||
};
|
||||
}
|
||||
|
||||
// UI
|
||||
|
||||
export const TOGGLE_FETCHING_DOMAIN = 'TOGGLE_FETCHING_DOMAIN';
|
||||
export function toggleFetchingDomain() {
|
||||
return {
|
||||
type: TOGGLE_FETCHING_DOMAIN
|
||||
};
|
||||
}
|
||||
|
||||
export const TOGGLE_FETCHING_EVENTS = 'TOGGLE_FETCHING_EVENTS';
|
||||
export function toggleFetchingEvents() {
|
||||
return {
|
||||
type: TOGGLE_FETCHING_EVENTS
|
||||
};
|
||||
}
|
||||
|
||||
export const TOGGLE_VIEW = 'TOGGLE_VIEW';
|
||||
export function toggleView() {
|
||||
return {
|
||||
type: TOGGLE_VIEW
|
||||
};
|
||||
}
|
||||
|
||||
export const TOGGLE_TIMELINE = 'TOGGLE_TIMELINE';
|
||||
export function toggleTimeline() {
|
||||
return {
|
||||
type: TOGGLE_TIMELINE
|
||||
};
|
||||
}
|
||||
|
||||
export const TOGGLE_LANGUAGE = 'TOGGLE_LANGUAGE';
|
||||
export function toggleLanguage(language) {
|
||||
return {
|
||||
type: TOGGLE_LANGUAGE,
|
||||
language,
|
||||
}
|
||||
}
|
||||
|
||||
export const OPEN_TOOLBAR = 'OPEN_TOOLBAR';
|
||||
export function openToolbar(toolbarTab = 0) {
|
||||
return {
|
||||
type: OPEN_TOOLBAR,
|
||||
toolbarTab: toolbarTab,
|
||||
};
|
||||
}
|
||||
|
||||
export const CLOSE_TOOLBAR = 'CLOSE_TOOLBAR';
|
||||
export function closeToolbar() {
|
||||
return {
|
||||
type: CLOSE_TOOLBAR
|
||||
};
|
||||
}
|
||||
|
||||
export const OPEN_CABINET = 'OPEN_CABINET';
|
||||
export function openCabinet(tabNum) {
|
||||
return {
|
||||
type: OPEN_CABINET,
|
||||
tabNum: tabNum,
|
||||
};
|
||||
}
|
||||
|
||||
export const CLOSE_CABINET = 'CLOSE_CABINET';
|
||||
export function closeCabinet() {
|
||||
return {
|
||||
type: CLOSE_CABINET
|
||||
};
|
||||
}
|
||||
|
||||
export const TOGGLE_INFOPOPUP = 'TOGGLE_INFOPOPUP';
|
||||
export function toggleInfoPopup() {
|
||||
return {
|
||||
type: TOGGLE_INFOPOPUP
|
||||
};
|
||||
}
|
||||
|
||||
export const TOGGLE_NOTIFICATIONS = 'TOGGLE_NOTIFICATIONS';
|
||||
export function toggleNotifications() {
|
||||
return {
|
||||
type: TOGGLE_NOTIFICATIONS
|
||||
};
|
||||
}
|
||||
8
src/assets/arrowdown.svg
Normal file
8
src/assets/arrowdown.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="595.28px" height="841.89px" viewBox="0 0 595.28 841.89" enable-background="new 0 0 595.28 841.89" xml:space="preserve">
|
||||
<line fill="#FFFFFF" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="1.688" y1="1.884" x2="7.303" y2="7.499"/>
|
||||
<line fill="#FFFFFF" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="7.303" y1="7.499" x2="12.803" y2="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 842 B |
7
src/assets/checkbox.svg
Normal file
7
src/assets/checkbox.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="595.28px" height="841.89px" viewBox="0 0 595.28 841.89" enable-background="new 0 0 595.28 841.89" xml:space="preserve">
|
||||
<rect x="1.076" y="0.983" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" width="15" height="15"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 669 B |
8
src/assets/close.svg
Normal file
8
src/assets/close.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="595.28px" height="841.89px" viewBox="0 0 595.28 841.89" enable-background="new 0 0 595.28 841.89" xml:space="preserve">
|
||||
<line fill="#FFFFFF" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="1.224" y1="1.125" x2="11.831" y2="11.732"/>
|
||||
<line fill="#FFFFFF" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="1.419" y1="11.537" x2="12.027" y2="0.93"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 848 B |
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;
|
||||
12
src/index.jsx
Normal file
12
src/index.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from './store/index.js';
|
||||
import App from './components/App.jsx';
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('explore-app')
|
||||
);
|
||||
144
src/js/data/copy.json
Normal file
144
src/js/data/copy.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"es-MX": {
|
||||
"loading": "Cargando...",
|
||||
"legend": {
|
||||
"view2d": {
|
||||
"paragraphs": [
|
||||
"Seleccionando una serie de filtros verá aparecer eventos en el mapa y en la línea del tiempo.",
|
||||
"Cada evento estará coloreado según la persona que dio el testimonio del evento."
|
||||
],
|
||||
"colors": [
|
||||
{ "class": "category_group00", "label": "Category Group 00" },
|
||||
{ "class": "category_group01", "label": "Category Group 01" },
|
||||
{ "class": "category_group02", "label": "Category Group 02" },
|
||||
{ "class": "category_group03", "label": "Category Group 03" },
|
||||
{ "class": "other", "label": "Other categories" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"title": "TITLE",
|
||||
"panels": {
|
||||
"mentions": {
|
||||
"title": "Personas",
|
||||
"overview": "Seleccionar los nombres de personas mostrará eventos en los que esta persona o organización ha sido mencionada, incluyendo el propio testimonio. Entre paréntesis encontrará el número de menciones. Ej. (34)."
|
||||
},
|
||||
"categories": {
|
||||
"title": "Testimonios",
|
||||
"overview": "Seleccionar el nombre de una persona mostrará los eventos descritos por su testimonio. Entre paréntesis encontrará el número de eventos descritos. Ej. (34)."
|
||||
},
|
||||
"search": {
|
||||
"title": "Directorio de etiquetas",
|
||||
"placeholder": "Búsqueda"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"zooms": [
|
||||
"3 años",
|
||||
"3 meses",
|
||||
"3 días",
|
||||
"12 horas",
|
||||
"2 horas",
|
||||
"30 min",
|
||||
"10 min"
|
||||
],
|
||||
"labels_title": "Testimonios",
|
||||
"labels": [
|
||||
"Testimony Group 00",
|
||||
"Testimony Group 01",
|
||||
"Testimony Group 02",
|
||||
"Testimony Group 03",
|
||||
"Other categories"
|
||||
],
|
||||
"info": "Viendo eventos ocurridos entre"
|
||||
},
|
||||
"cardstack": {
|
||||
"header": "eventos seleccionados",
|
||||
"unknown_location": "Localización desconocida",
|
||||
"timestamp": "Día y hora",
|
||||
"estimated": "aproximado",
|
||||
"location": "Localización",
|
||||
"incident_type": "Tipo de acción",
|
||||
"description": "Hechos",
|
||||
"people": "Personas en el evento",
|
||||
"source": "Fuente",
|
||||
"category": "Según el testimonio de",
|
||||
"communication": "Comunicación",
|
||||
"transmitter": "Transmisor",
|
||||
"receiver": "Receptor",
|
||||
"warning": "(!) HECHOS CUESTIONADOS"
|
||||
}
|
||||
},
|
||||
"en-US": {
|
||||
"loading": "Loading...",
|
||||
"legend": {
|
||||
"view2d": {
|
||||
"paragraphs": [
|
||||
"Selecting a series of tags, you will be able to explore events on the map of Iguala and on the timeline.",
|
||||
"Each event is colored according the person that gave category of the event."
|
||||
],
|
||||
"colors": [
|
||||
{ "class": "category_group00", "label": "Category Group 00" },
|
||||
{ "class": "category_group01", "label": "Category Group 01" },
|
||||
{ "class": "category_group02", "label": "Category Group 02" },
|
||||
{ "class": "category_group03", "label": "Category Group 03" },
|
||||
{ "class": "other", "label": "Other categories" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"title": "TITLE",
|
||||
"panels": {
|
||||
"mentions": {
|
||||
"title": "Mentions",
|
||||
"overview": "Selecting the names of people/organisation will show events in which these have been mentioned in their own testistimony and by others. The number in the parentheses shows how many events contain a mention of a person or organisation, e.g. (34)"
|
||||
},
|
||||
"categories": {
|
||||
"title": "Testimonies",
|
||||
"overview": "Selecting the name of a person will show the events only according to a person’s category or category. The number in the parentheses show how many events are contained in each category, e.g. (34)."
|
||||
},
|
||||
"search": {
|
||||
"title": "Directory of tags",
|
||||
"placeholder": "Search"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"zooms": [
|
||||
"3 years",
|
||||
"3 months",
|
||||
"3 days",
|
||||
"12 hours",
|
||||
"2 hours",
|
||||
"30 min",
|
||||
"10 min"
|
||||
],
|
||||
"labels_title": "Testimonies",
|
||||
"labels": [
|
||||
"Testimony Group 00",
|
||||
"Testimony Group 01",
|
||||
"Testimony Group 02",
|
||||
"Testimony Group 03",
|
||||
"Other"
|
||||
],
|
||||
"info": "Seeing events occurred between"
|
||||
},
|
||||
"cardstack": {
|
||||
"header": "selected events",
|
||||
"timestamp": "Day and time",
|
||||
"unknown_location": "Unknown location",
|
||||
"estimated": "estimated",
|
||||
"location": "Localization",
|
||||
"incident_type": "Type of action",
|
||||
"description": "Summary of facts",
|
||||
"people": "People involved",
|
||||
"source": "Source",
|
||||
"category": "According to",
|
||||
"communication": "Communication",
|
||||
"transmitter": "Transmitter",
|
||||
"receiver": "Receiver",
|
||||
"warning": "(!) Highly questioned"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/js/data/es-MX.json
Normal file
10
src/js/data/es-MX.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"dateTime": "%x, %X",
|
||||
"date": "%d/%m/%Y",
|
||||
"time": "%-I:%M:%S %p",
|
||||
"periods": ["AM", "PM"],
|
||||
"days": ["domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"],
|
||||
"shortDays": ["dom", "lun", "mar", "mié", "jue", "vie", "sáb"],
|
||||
"months": ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"],
|
||||
"shortMonths": ["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"]
|
||||
}
|
||||
37
src/js/data/utilities.js
Normal file
37
src/js/data/utilities.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Get URI params to start with predefined set of
|
||||
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||
* @param {string} name: name of paramater to search
|
||||
* @param {string} url: url passed as variable, defaults to window.location.href
|
||||
*/
|
||||
export function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[\[\]]/g, `\\$&`);
|
||||
|
||||
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
|
||||
const results = regex.exec(url);
|
||||
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two arrays of scalars
|
||||
* @param {array} arr1: array of numbers
|
||||
* @param {array} arr2: array of numbers
|
||||
*/
|
||||
export function areEqual(arr1, arr2) {
|
||||
return ((arr1.length === arr2.length) && arr1.every((element, index) => {
|
||||
return element === arr2[index];
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the variable is neither null nor undefined
|
||||
* @param {object} variable
|
||||
*/
|
||||
export function isNotNullNorUndefined(variable) {
|
||||
return (typeof variable !== 'undefined' && variable !== null);
|
||||
}
|
||||
393
src/js/map/map.js
Normal file
393
src/js/map/map.js
Normal file
@@ -0,0 +1,393 @@
|
||||
import {
|
||||
areEqual,
|
||||
isNotNullNorUndefined
|
||||
} from '../data/utilities';
|
||||
import hash from 'object-hash';
|
||||
import 'leaflet-polylinedecorator';
|
||||
|
||||
export default function(newApp, ui, select) {
|
||||
let svg, g, defs;
|
||||
|
||||
let categoryColorGroups = {};
|
||||
|
||||
const domain = {
|
||||
locations: [],
|
||||
categoryGroups: [],
|
||||
sites: []
|
||||
}
|
||||
const app = {
|
||||
selected: [],
|
||||
highlighted: null,
|
||||
views: Object.assign({}, newApp.views),
|
||||
}
|
||||
|
||||
const getCategoryGroup = newApp.getCategoryGroup;
|
||||
const getCategoryGroupColor = newApp.getCategoryGroupColor;
|
||||
const groupColors = ui.style.groupColors;
|
||||
|
||||
// Map Settings
|
||||
const center = newApp.mapAnchor;
|
||||
const maxBoundaries = [[180, -180], [-180, 180]];
|
||||
const zoomLevel = 14;
|
||||
|
||||
// Initialize layer
|
||||
const sitesLayer = L.layerGroup();
|
||||
const pathLayer = L.layerGroup();
|
||||
|
||||
// Icons for markPoint flags (a yellow ring around a location)
|
||||
const eventCircleMarkers = {};
|
||||
|
||||
// Styles for elements in map
|
||||
const settingsSiteLabel = {
|
||||
className: 'site-label',
|
||||
opacity: 1,
|
||||
permanent: true,
|
||||
direction: 'top',
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a Leaflet map and a tilelayer for the map background
|
||||
* @param {string} id: DOM element to create map onto
|
||||
* @param {array} center: [lat, long] coordinates the map will be centered on
|
||||
* @param {number} zoom: zoom level
|
||||
*/
|
||||
function initBackgroundMap(id, zoom) {
|
||||
/* http://bl.ocks.org/sumbera/10463358 */
|
||||
|
||||
const map = L.map(id)
|
||||
.setView(center, zoom)
|
||||
.setMinZoom(10)
|
||||
.setMaxZoom(18)
|
||||
.setMaxBounds(maxBoundaries);
|
||||
|
||||
// NB: configure tile endpoint
|
||||
let s;
|
||||
if (process.env.MAPBOX_TOKEN) {
|
||||
s = L.tileLayer(
|
||||
`http://a.tiles.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.png?access_token=${process.env.MAPBOX_TOKEN}`
|
||||
);
|
||||
} else {
|
||||
s = L.tileLayer(`${process.env.SERVER_ROOT}/mapbox/{z}/{x}/{y}`);
|
||||
}
|
||||
s = s.addTo(map);
|
||||
|
||||
map.keyboard.disable();
|
||||
const pane = d3.select(map.getPanes().overlayPane);
|
||||
const boundingClient = d3.select(`#${id}`).node().getBoundingClientRect();
|
||||
const width = boundingClient.width;
|
||||
const height = boundingClient.height;
|
||||
|
||||
svg = pane.append('svg')
|
||||
.attr('class', 'leaflet-svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
g = svg.append('g');
|
||||
|
||||
svg.insert('defs', 'g')
|
||||
.append('marker')
|
||||
.attr('id', 'arrow')
|
||||
.attr('viewBox', '0 0 6 6')
|
||||
.attr('refX', 3)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 14)
|
||||
.attr('markerHeight', 14)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,3v-3l6,3l-6,3z');
|
||||
|
||||
map.on('zoomstart', () => {
|
||||
svg.classed('hide', true);
|
||||
});
|
||||
map.on('zoomend', () => {
|
||||
svg.classed('hide', false);
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
// Initialize leaflet map and layers for each type of data
|
||||
const lMap = initBackgroundMap(ui.dom.map, zoomLevel);
|
||||
|
||||
function projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return lMap.latLngToLayerPoint(latLng);
|
||||
}
|
||||
|
||||
function getSVGBoundaries() {
|
||||
return {
|
||||
topLeft: projectPoint(maxBoundaries[0]),
|
||||
bottomRight: projectPoint(maxBoundaries[1])
|
||||
}
|
||||
}
|
||||
|
||||
function updateSVG() {
|
||||
const boundaries = getSVGBoundaries();
|
||||
const {
|
||||
topLeft,
|
||||
bottomRight
|
||||
} = boundaries;
|
||||
svg.attr('width', bottomRight.x - topLeft.x + 200)
|
||||
.attr('height', bottomRight.y - topLeft.y + 200)
|
||||
.style('left', `${topLeft.x - 100}px`)
|
||||
.style('top', `${topLeft.y - 100}px`);
|
||||
|
||||
g.attr('transform', `translate(${-(topLeft.x - 100)},${-(topLeft.y - 100)})`);
|
||||
|
||||
g.selectAll('.location').attr('transform', (d) => {
|
||||
const newPoint = projectPoint([+d.latitude, +d.longitude]);
|
||||
return `translate(${newPoint.x},${newPoint.y})`;
|
||||
});
|
||||
|
||||
const busLine = d3.line()
|
||||
.x(d => lMap.latLngToLayerPoint(d).x)
|
||||
.y(d => lMap.latLngToLayerPoint(d).y)
|
||||
.curve(d3.curveMonotoneX);
|
||||
}
|
||||
|
||||
lMap.on("zoom viewreset move", updateSVG);
|
||||
|
||||
/**
|
||||
* Returns latitud / longitude
|
||||
* @param {Object} eventPoint: data for an evenPoint - time, loc, tags, etc
|
||||
*/
|
||||
function getEventLocation(eventPoint) {
|
||||
return {
|
||||
latitude: +eventPoint.location.latitude,
|
||||
longitude: +eventPoint.location.longitude,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* INTERACTIVE FUNCTIONS
|
||||
*/
|
||||
|
||||
/**
|
||||
* Removes the circular ring to mark a particular location
|
||||
*/
|
||||
function unmarkPoint() {
|
||||
Object.keys(eventCircleMarkers).forEach(markerId => {
|
||||
lMap.removeLayer(eventCircleMarkers[markerId]);
|
||||
delete eventCircleMarkers[markerId];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a circular ring mark in one particular location at a time
|
||||
* @param {object} location object, with lat and long
|
||||
*/
|
||||
function renderSelected() {
|
||||
unmarkPoint();
|
||||
app.selected.forEach(eventPoint => {
|
||||
if (isNotNullNorUndefined(eventPoint) && isNotNullNorUndefined(eventPoint.location)) {
|
||||
if (eventPoint.latitude && eventPoint.longitude) {
|
||||
const location = new L.LatLng(eventPoint.latitude, eventPoint.longitude);
|
||||
eventCircleMarkers[eventPoint.id] = L.circleMarker(location, {
|
||||
radius: 32,
|
||||
fill: false,
|
||||
color: '#ffffff',
|
||||
weight: 3,
|
||||
lineCap: '',
|
||||
dashArray: '5,2'
|
||||
});
|
||||
eventCircleMarkers[eventPoint.id].addTo(lMap);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function renderHighlighted() {
|
||||
// Fly to first of events selected
|
||||
const eventPoint = (app.selected.length > 0) ? app.selected[0] : null;
|
||||
if (isNotNullNorUndefined(eventPoint) && isNotNullNorUndefined(eventPoint.location)) {
|
||||
if (eventPoint.latitude && eventPoint.longitude) {
|
||||
const location = new L.LatLng(eventPoint.latitude, eventPoint.longitude);
|
||||
lMap.flyTo(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* RENDERING FUNCTIONS
|
||||
*/
|
||||
|
||||
function getLocationEventsDistribution(location) {
|
||||
const eventsHere = {};
|
||||
const categoryGroups = domain.categoryGroups;
|
||||
categoryGroups.sort((a, b) => {
|
||||
return (+a.slice(-2) > +b.slice(-2));
|
||||
});
|
||||
categoryGroups.forEach(group => {
|
||||
eventsHere[group] = 0
|
||||
});
|
||||
|
||||
location.events.forEach((event) => {
|
||||
const group = getCategoryGroup(event.category);
|
||||
eventsHere[group] += 1;
|
||||
});
|
||||
|
||||
let i = 0;
|
||||
const events = [];
|
||||
|
||||
while (i < categoryGroups.length) {
|
||||
let eventsCount = eventsHere[categoryGroups[i]];
|
||||
for (let j = i + 1; j < categoryGroups.length; j++) {
|
||||
eventsCount += eventsHere[categoryGroups[j]];
|
||||
}
|
||||
events.push(eventsCount);
|
||||
i++;
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears existing event layer
|
||||
* Renders all events as markers
|
||||
* Adds eventlayer to map
|
||||
*/
|
||||
function renderEvents() {
|
||||
const locationsDom = g.selectAll('.location')
|
||||
.data(domain.locations, d => d.id)
|
||||
|
||||
locationsDom
|
||||
.exit()
|
||||
.remove();
|
||||
|
||||
locationsDom
|
||||
.enter().append('g')
|
||||
.attr('class', 'location')
|
||||
.attr('transform', (d) => {
|
||||
d.LatLng = new L.LatLng(+d.latitude, +d.longitude);
|
||||
return `translate(${lMap.latLngToLayerPoint(d.LatLng).x},
|
||||
${lMap.latLngToLayerPoint(d.LatLng).y})`;
|
||||
})
|
||||
.on('click', (location) => {
|
||||
select(location.events);
|
||||
});
|
||||
|
||||
const eventsDom = g.selectAll('.location').selectAll('.location-event-marker')
|
||||
.data((d, i) => getLocationEventsDistribution(domain.locations[i]),
|
||||
(d, i) => 'location-' + i);
|
||||
|
||||
eventsDom
|
||||
.exit()
|
||||
.attr('r', 0)
|
||||
.remove();
|
||||
|
||||
eventsDom
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('r', d => (d) ? Math.sqrt(16 * d) + 3 : 0);
|
||||
|
||||
eventsDom
|
||||
.enter().append('circle')
|
||||
.attr('class', 'location-event-marker')
|
||||
.style('fill', (d, i) => groupColors[domain.categoryGroups[i]])
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('r', d => (d) ? Math.sqrt(16 * d) + 3 : 0);
|
||||
}
|
||||
|
||||
// NB: is this a function to be removed for future features?
|
||||
function renderSites() {
|
||||
sitesLayer.clearLayers();
|
||||
lMap.removeLayer(sitesLayer);
|
||||
|
||||
// Create a label for each attack site, persistent across filtering
|
||||
if (app.views.sites) {
|
||||
domain.sites.forEach((site) => {
|
||||
if (isNotNullNorUndefined(site)) {
|
||||
// Create an invisible marker for each site label
|
||||
const siteMarker = L.circleMarker([+site.latitude, +site.longitude], {
|
||||
radius: 0,
|
||||
stroke: 0
|
||||
});
|
||||
|
||||
siteMarker.bindTooltip(site.site, settingsSiteLabel).openTooltip();
|
||||
|
||||
// Add this one attack marker to group attack layer
|
||||
sitesLayer.addLayer(siteMarker);
|
||||
}
|
||||
});
|
||||
|
||||
lMap.addLayer(sitesLayer);
|
||||
}
|
||||
}
|
||||
|
||||
// NB: is this a function to be removed for future features?
|
||||
/**
|
||||
* Creats a marker for an eventPoint along a path
|
||||
* @param {Object} eventPoint: data for an evenPoint - time, loc, tags, etc
|
||||
* @param {number} step: the portion of the entire path this event corresponds to
|
||||
*/
|
||||
function createPathEventMarker(eventPoint, step) {
|
||||
const {
|
||||
latitude,
|
||||
longitude
|
||||
} = getEventLocation(eventPoint);
|
||||
const pathEventMarker = L.circleMarker(
|
||||
[latitude, longitude], {
|
||||
color: ui.colors.DARKGREY,
|
||||
fill: ui.colors.DARKGREY,
|
||||
weight: 2,
|
||||
fillOpacity: 0.6,
|
||||
radius: 10 * step,
|
||||
},
|
||||
);
|
||||
|
||||
// Add marker event handlers
|
||||
pathEventMarker.bindPopup('');
|
||||
pathEventMarker.on('popupopen', () => {
|
||||
select([eventPoint]);
|
||||
});
|
||||
pathEventMarker.on('popupclose', () => {
|
||||
select();
|
||||
});
|
||||
|
||||
return pathEventMarker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates displayable data on the map: events, coevents and paths
|
||||
* @param {Object} domain: object of arrays of events, coevs, attacks, paths, sites
|
||||
*/
|
||||
function update(newDomain, newApp) {
|
||||
updateSVG();
|
||||
|
||||
if (hash(domain) !== hash(newDomain)) {
|
||||
domain.locations = newDomain.locations;
|
||||
domain.categoryGroups = newDomain.categoryGroups;
|
||||
domain.sites = newDomain.sites;
|
||||
renderDomain();
|
||||
}
|
||||
|
||||
if (hash(app) !== hash(newApp)) {
|
||||
app.selected = newApp.selected;
|
||||
app.highlighted = newApp.highlighted;
|
||||
app.views = newApp.views;
|
||||
|
||||
renderSelectedAndHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders events on the map: takes data, and enters, updates and exits
|
||||
*/
|
||||
function renderDomain () {
|
||||
renderSites();
|
||||
renderEvents();
|
||||
}
|
||||
function renderSelectedAndHighlight () {
|
||||
renderSelected();
|
||||
renderHighlighted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose only relevant functions
|
||||
*/
|
||||
return {
|
||||
update
|
||||
};
|
||||
}
|
||||
645
src/js/timeline/timeline.js
Normal file
645
src/js/timeline/timeline.js
Normal file
@@ -0,0 +1,645 @@
|
||||
/*
|
||||
TIMELINE
|
||||
Displays events over the course of the night
|
||||
Allows brushing and selecting periods of time in it
|
||||
TODO: is it possible to express this idiomatically as React?
|
||||
*/
|
||||
import {
|
||||
areEqual
|
||||
} from '../data/utilities';
|
||||
import esLocale from '../data/es-MX.json';
|
||||
import copy from '../data/copy.json';
|
||||
|
||||
export default function(app, ui) {
|
||||
d3.timeFormatDefaultLocale(esLocale);
|
||||
const formatterWithYear = ui.tools.formatterWithYear;
|
||||
const parser = ui.tools.parser;
|
||||
const zoomLevels = [{
|
||||
label: '3 años',
|
||||
duration: 1576800,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '3 meses',
|
||||
duration: 129600,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '3 días',
|
||||
duration: 4320,
|
||||
active: true
|
||||
},
|
||||
{
|
||||
label: '12 horas',
|
||||
duration: 720,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '2 horas',
|
||||
duration: 120,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '30 min',
|
||||
duration: 30,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
label: '10 min',
|
||||
duration: 10,
|
||||
active: false
|
||||
},
|
||||
];
|
||||
|
||||
let events = [];
|
||||
let categoryGroups = [];
|
||||
let selected = [];
|
||||
let range = app.range;
|
||||
|
||||
const filter = app.filter;
|
||||
const select = app.select;
|
||||
const getCategoryLabel = app.getCategoryLabel;
|
||||
const getCategoryGroupColor = app.getCategoryGroupColor;
|
||||
const getCategoryGroup = app.getCategoryGroup;
|
||||
|
||||
// Play functions
|
||||
window.playInterval;
|
||||
let isPlaying = false;
|
||||
const playDuration = 1000;
|
||||
|
||||
// Drag behavior
|
||||
let dragPos0;
|
||||
let transitionDuration = 500;
|
||||
|
||||
// Dimension of the client
|
||||
const WIDTH_CONTROLS = 180;
|
||||
const boundingClient = d3.select(`#${ui.dom.timeline}`).node().getBoundingClientRect();
|
||||
let WIDTH = boundingClient.width - WIDTH_CONTROLS;
|
||||
const HEIGHT = 140;
|
||||
const markerRadius = 15;
|
||||
// margin
|
||||
// NB: is it possible to do this with SCSS?
|
||||
// A: Maybe, although we are using it programmatically here for now
|
||||
const mg = {
|
||||
l: 120
|
||||
};
|
||||
|
||||
/**
|
||||
* Create scales
|
||||
*/
|
||||
const scale = {};
|
||||
|
||||
scale.x = d3.scaleTime()
|
||||
.domain(range)
|
||||
.range([mg.l, WIDTH]);
|
||||
|
||||
const groupStep = (106 - 30) / categoryGroups.length;
|
||||
const groupYs = new Array(categoryGroups.length);
|
||||
groupYs.map((g, i) => {
|
||||
return 30 + i * groupStep;
|
||||
});
|
||||
|
||||
scale.y = d3.scaleOrdinal()
|
||||
.domain(categoryGroups)
|
||||
.range(groupYs);
|
||||
|
||||
/**
|
||||
* Initilize SVG elements and groups
|
||||
*/
|
||||
const dom = {};
|
||||
|
||||
dom.svg =
|
||||
d3.select(`#${ui.dom.timeline}`)
|
||||
.append('svg')
|
||||
.attr('width', WIDTH)
|
||||
.attr('height', HEIGHT);
|
||||
|
||||
dom.controls =
|
||||
d3.select(`#${ui.dom.timeline}`)
|
||||
.append('svg')
|
||||
.attr('class', 'time-controls')
|
||||
.attr('width', WIDTH_CONTROLS)
|
||||
.attr('height', HEIGHT);
|
||||
|
||||
/*
|
||||
* Axis group elements
|
||||
*/
|
||||
|
||||
dom.axis = {};
|
||||
|
||||
dom.axis.x0 = dom.svg.append('g')
|
||||
.attr('transform', `translate(0, 25)`)
|
||||
.attr('class', 'axis xAxis');
|
||||
|
||||
dom.axis.x1 = dom.svg.append('g')
|
||||
.attr('transform', `translate(0, 105)`)
|
||||
.attr('class', 'axis axisHourText');
|
||||
|
||||
dom.axis.y = dom.svg.append('g')
|
||||
.attr('transform', `translate(${WIDTH}, 0)`)
|
||||
.attr('class', 'yAxis');
|
||||
|
||||
dom.axis.boundaries = dom.svg.selectAll('.axisBoundaries')
|
||||
.data([0, 1])
|
||||
.enter().append('line')
|
||||
.attr('class', 'axisBoundaries');
|
||||
|
||||
dom.axis.label0 = dom.svg.append('text')
|
||||
.attr('class', 'timeLabel0 timeLabel');
|
||||
|
||||
dom.axis.label1 = dom.svg.append('text')
|
||||
.attr('class', 'timelabelF timeLabel');
|
||||
|
||||
/*
|
||||
* Plottable elements
|
||||
*/
|
||||
dom.dataset = dom.svg.append('g');
|
||||
dom.events = dom.dataset.append('g');
|
||||
|
||||
/*
|
||||
* Time Controls
|
||||
*/
|
||||
dom.forward = dom.svg.append('g').attr('class', 'time-controls-inline');
|
||||
dom.forward.append('circle');
|
||||
dom.forward.append('path');
|
||||
|
||||
dom.backwards = dom.svg.append('g').attr('class', 'time-controls-inline');
|
||||
dom.backwards.append('circle');
|
||||
dom.backwards.append('path');
|
||||
|
||||
dom.playGroup = dom.controls.append('g');
|
||||
dom.playGroup.append('circle');
|
||||
|
||||
dom.play = dom.playGroup.append('g');
|
||||
dom.play.append('path');
|
||||
|
||||
dom.pause = dom.playGroup.append('g').style('opacity', 0);
|
||||
dom.pause.append('rect');
|
||||
dom.pause.append('rect');
|
||||
|
||||
dom.zooms = dom.controls.append('g');
|
||||
|
||||
dom.zooms.selectAll('.zoom-level-button')
|
||||
.data(zoomLevels)
|
||||
.enter().append('text')
|
||||
.attr('class', 'zoom-level-button');
|
||||
|
||||
/*
|
||||
* Initialize axis function and element group
|
||||
*/
|
||||
const axis = {};
|
||||
|
||||
axis.x0 =
|
||||
d3.axisBottom(scale.x)
|
||||
.ticks(10)
|
||||
.tickPadding(5)
|
||||
.tickSize(80)
|
||||
.tickFormat(d3.timeFormat('%d %b'));
|
||||
|
||||
axis.x1 =
|
||||
d3.axisBottom(scale.x)
|
||||
.ticks(10)
|
||||
.tickPadding(20)
|
||||
.tickSize(0)
|
||||
.tickFormat(d3.timeFormat('%H:%M'));
|
||||
|
||||
axis.y =
|
||||
d3.axisLeft(scale.y)
|
||||
.tickValues([]);
|
||||
|
||||
/*
|
||||
* Setup drag behavior
|
||||
*/
|
||||
const drag =
|
||||
d3.drag()
|
||||
.on('start', () => {
|
||||
d3.event.sourceEvent.stopPropagation();
|
||||
dragPos0 = d3.event.x;
|
||||
toggleTransition(false);
|
||||
})
|
||||
.on('drag', () => {
|
||||
const drag0 = scale.x.invert(dragPos0).getTime();
|
||||
const dragNow = scale.x.invert(d3.event.x).getTime();
|
||||
const timeShift = (drag0 - dragNow) / 1000;
|
||||
|
||||
const newDomain0 = d3.timeSecond.offset(range[0], timeShift);
|
||||
const newDomainF = d3.timeSecond.offset(range[1], timeShift);
|
||||
|
||||
scale.x.domain([newDomain0, newDomainF])
|
||||
render();
|
||||
})
|
||||
.on('end', () => {
|
||||
toggleTransition(true);
|
||||
filter({
|
||||
range: scale.x.domain()
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* SVG groups for marker
|
||||
*/
|
||||
|
||||
dom.markers = dom.svg.append('g');
|
||||
|
||||
/**
|
||||
* Adapt dimensions when resizing
|
||||
*/
|
||||
function getCurrentWidth() {
|
||||
return d3.select(`#${ui.dom.timeline}`).node()
|
||||
.getBoundingClientRect().width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize timeline one window resice
|
||||
*/
|
||||
function addResizeListener() {
|
||||
window.addEventListener('resize', () => {
|
||||
if (d3.select(`#${ui.dom.timeline}`).node() !== null) {
|
||||
WIDTH = getCurrentWidth() - WIDTH_CONTROLS;
|
||||
|
||||
dom.svg.attr('width', WIDTH);
|
||||
scale.x.range([mg.l, WIDTH]);
|
||||
axis.y.tickSize(WIDTH - mg.l);
|
||||
dom.axis.y.attr('transform', `translate(${WIDTH}, 0)`)
|
||||
render(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
addResizeListener();
|
||||
|
||||
/**
|
||||
* PLAY FUNCTIONALITY
|
||||
*/
|
||||
function stopBrushTransition() {
|
||||
clearInterval(window.playInterval);
|
||||
isPlaying = false;
|
||||
dom.play.style('opacity', 1);
|
||||
dom.pause.style('opacity', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* START PLAY SERIES OF TRANSITIONS
|
||||
*/
|
||||
function playBrushTransition() {
|
||||
isPlaying = true;
|
||||
dom.play.style('opacity', 0);
|
||||
dom.pause.style('opacity', 1);
|
||||
window.playInterval = setInterval(() => {
|
||||
moveTime('forward');
|
||||
}, playDuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return which color event circle should be based on incident type
|
||||
* @param {object} eventPoint data object
|
||||
*/
|
||||
function getEventPointFillColor(eventPoint) {
|
||||
return getCategoryGroupColor(eventPoint.category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event, get all the filtered events that happen simultaneously
|
||||
* @param {object} eventPoint: regular eventPoint data
|
||||
*/
|
||||
function getAllEventsAtOnce(eventPoint) {
|
||||
const timestamp = eventPoint.timestamp;
|
||||
const categoryGroup = getCategoryGroup(eventPoint.category);
|
||||
return events.filter(event => {
|
||||
return (event.timestamp === timestamp &&
|
||||
categoryGroup === getCategoryGroup(event.category))
|
||||
}).map(event => event.id);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get y height of eventPoint, considering the ordinal Y scale
|
||||
* @param {object} eventPoint: regular eventPoint data
|
||||
*/
|
||||
function getEventY(eventPoint) {
|
||||
const yGroup = getCategoryGroup(eventPoint.category);
|
||||
return scale.y(yGroup);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get x position of eventPoint, considering the time scale
|
||||
* @param {object} eventPoint: regular eventPoint data
|
||||
*/
|
||||
function getEventX(eventPoint) {
|
||||
return scale.x(parser(eventPoint.timestamp));
|
||||
}
|
||||
|
||||
function getTimeScaleExtent() {
|
||||
return (scale.x.domain()[1].getTime() - scale.x.domain()[0].getTime()) / 60000;
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a number of minutes, calculate the width based on current scale.x
|
||||
* @param {number} minutes: number of minutes
|
||||
*/
|
||||
function getWidthOfTime(minutes) {
|
||||
const allMins = getTimeScaleExtent();
|
||||
return (minutes * WIDTH) / allMins;
|
||||
}
|
||||
|
||||
function highlightZoomLevel(zoom) {
|
||||
zoomLevels.forEach((level) => {
|
||||
if (level.label === zoom.label) {
|
||||
level.active = true;
|
||||
} else {
|
||||
level.active = false;
|
||||
}
|
||||
});
|
||||
|
||||
dom.zooms.selectAll('text')
|
||||
.classed('active', level => level.active);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply zoom level to timeline
|
||||
* @param {object} zoom: zoom level from zoomLevels
|
||||
*/
|
||||
function applyZoom(zoom) {
|
||||
highlightZoomLevel(zoom);
|
||||
|
||||
const extent = getTimeScaleExtent();
|
||||
const newCentralTime = d3.timeMinute.offset(scale.x.domain()[0], extent / 2);
|
||||
|
||||
const domain0 = d3.timeMinute.offset(newCentralTime, -zoom.duration / 2);
|
||||
const domainF = d3.timeMinute.offset(newCentralTime, zoom.duration / 2);
|
||||
|
||||
scale.x.domain([domain0, domainF]);
|
||||
filter({
|
||||
range: scale.x.domain()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shift time range by moving forward or backwards
|
||||
* @param {Stirng} direction: 'forward' / 'backwards'
|
||||
*/
|
||||
function moveTime(direction) {
|
||||
select();
|
||||
const extent = getTimeScaleExtent();
|
||||
const newCentralTime = d3.timeMinute.offset(scale.x.domain()[0], extent / 2);
|
||||
|
||||
// if forward
|
||||
let domain0 = newCentralTime;
|
||||
let domainF = d3.timeMinute.offset(newCentralTime, extent);
|
||||
|
||||
// if backwards
|
||||
if (direction === 'backwards') {
|
||||
domain0 = d3.timeMinute.offset(newCentralTime, -extent);
|
||||
domainF = newCentralTime;
|
||||
}
|
||||
|
||||
scale.x.domain([domain0, domainF]);
|
||||
filter({
|
||||
range: scale.x.domain()
|
||||
});
|
||||
}
|
||||
|
||||
function toggleTransition(isTransition) {
|
||||
transitionDuration = (isTransition) ? 500 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight event circle on hover
|
||||
*/
|
||||
function handleMouseOver() {
|
||||
d3.select(this)
|
||||
.attr('r', 7)
|
||||
.classed('mouseover', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unhighlight event when mouse out
|
||||
*/
|
||||
function handleMouseOut() {
|
||||
d3.select(this)
|
||||
.attr('r', 5)
|
||||
.classed('mouseover', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* It automatically sets brush timeline to a domain set by the params
|
||||
*/
|
||||
function updateTimeRange() {
|
||||
scale.x.domain(range);
|
||||
axis.x0.scale(scale.x);
|
||||
axis.x1.scale(scale.x);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display the current time range in the time label above the timeline
|
||||
*/
|
||||
function renderTimeLabels() {
|
||||
dom.axis.label0
|
||||
.attr('x', 5)
|
||||
.attr('y', 15)
|
||||
.text(formatterWithYear(range[0]));
|
||||
|
||||
dom.axis.label1
|
||||
.attr('x', WIDTH - 5)
|
||||
.attr('y', 15)
|
||||
.text(formatterWithYear(range[1]))
|
||||
.style('text-anchor', 'end');
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a circular rinig mark in one particular location at a time
|
||||
* @param {object} eventPoint: object with eventPoint data (time, loc, tags)
|
||||
*/
|
||||
function renderHighlight() {
|
||||
const markers = dom.markers
|
||||
.selectAll('circle')
|
||||
.data(selected);
|
||||
|
||||
markers
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', 'timeline-marker')
|
||||
.merge(markers)
|
||||
.attr('cy', eventPoint => getEventY(eventPoint))
|
||||
.attr('cx', eventPoint => getEventX(eventPoint))
|
||||
.attr('r', 10)
|
||||
.style('opacity', .9);
|
||||
|
||||
markers.exit().remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return event circles of different groups
|
||||
*/
|
||||
function renderEvents() {
|
||||
const eventsDom = dom.events
|
||||
.selectAll('.event')
|
||||
.data(events, d => d.id);
|
||||
|
||||
eventsDom
|
||||
.exit()
|
||||
.remove();
|
||||
|
||||
eventsDom
|
||||
.transition()
|
||||
.duration(transitionDuration)
|
||||
.attr('cx', eventPoint => getEventX(eventPoint));
|
||||
|
||||
eventsDom
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', 'event')
|
||||
.attr('cx', eventPoint => getEventX(eventPoint))
|
||||
.attr('cy', eventPoint => getEventY(eventPoint))
|
||||
.style('fill', eventPoint => getEventPointFillColor(eventPoint))
|
||||
.on('click', eventPoint => select(getAllEventsAtOnce(eventPoint)))
|
||||
.on('mouseover', handleMouseOver)
|
||||
.on('mouseout', handleMouseOut)
|
||||
.transition()
|
||||
.delay(300)
|
||||
.duration(200)
|
||||
.attr('r', 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render axis on timeline and viewbox boundaries
|
||||
*/
|
||||
function renderAxis() {
|
||||
dom.axis.x0
|
||||
.call(drag);
|
||||
|
||||
dom.axis.x1
|
||||
.call(drag);
|
||||
|
||||
dom.axis.x0
|
||||
.transition()
|
||||
.duration(transitionDuration)
|
||||
.call(axis.x0);
|
||||
|
||||
dom.axis.x1
|
||||
.transition()
|
||||
.duration(transitionDuration)
|
||||
.call(axis.x1);
|
||||
|
||||
axis.y.tickSize(WIDTH - mg.l);
|
||||
|
||||
dom.axis.y
|
||||
.call(axis.y)
|
||||
.call(drag);
|
||||
|
||||
dom.axis.boundaries
|
||||
.attr('x1', (d, i) => scale.x.range()[i])
|
||||
.attr('x2', (d, i) => scale.x.range()[i])
|
||||
.attr('y1', 10)
|
||||
.attr('y2', 20);
|
||||
|
||||
dom.axis.label1
|
||||
.attr('x', scale.x.range()[1] - 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render left and right time shifting controls
|
||||
*/
|
||||
function renderTimeControls() {
|
||||
const zoomLabels = copy[app.language].timeline.zooms;
|
||||
zoomLevels.forEach((level, i) => {
|
||||
level.label = zoomLabels[i];
|
||||
});
|
||||
|
||||
// These controls on timeline svg
|
||||
dom.backwards.select('circle')
|
||||
.attr('transform', `translate(${scale.x.range()[0] + 20}, 62)`)
|
||||
.attr('r', 15);
|
||||
|
||||
dom.backwards.select('path')
|
||||
.attr('d', d3.symbol().type(d3.symbolTriangle).size(80))
|
||||
.attr('transform', `translate(${scale.x.range()[0] + 20}, 62)rotate(270)`);
|
||||
|
||||
dom.forward.select('circle')
|
||||
.attr('transform', `translate(${scale.x.range()[1] - 20}, 62)`)
|
||||
.attr('r', 15);
|
||||
|
||||
dom.forward.select('path')
|
||||
.attr('d', d3.symbol().type(d3.symbolTriangle).size(80))
|
||||
.attr('transform', `translate(${scale.x.range()[1] - 20}, 62)rotate(90)`);
|
||||
|
||||
// These controls on separate svg
|
||||
dom.playGroup.select('circle')
|
||||
.attr('transform', 'translate(135, 60)rotate(90)')
|
||||
.attr('r', 25);
|
||||
|
||||
dom.play.select('path')
|
||||
.attr('d', d3.symbol().type(d3.symbolTriangle).size(260))
|
||||
.attr('transform', 'translate(135, 60)rotate(90)');
|
||||
|
||||
dom.pause.selectAll('rect')
|
||||
.attr('transform', (d, i) => `translate(${125 + (i * 15)}, 47)`)
|
||||
.attr('height', 25)
|
||||
.attr('width', 5);
|
||||
|
||||
dom.zooms.selectAll('text')
|
||||
.text(d => d.label)
|
||||
.attr('x', 60)
|
||||
.attr('y', (d, i) => (i * 15) + 20)
|
||||
.classed('active', level => level.active);
|
||||
|
||||
dom.forward
|
||||
.on('click', () => moveTime('forward'));
|
||||
|
||||
dom.backwards
|
||||
.on('click', () => moveTime('backwards'));
|
||||
|
||||
dom.playGroup
|
||||
.on('click', () => {
|
||||
return (isPlaying) ? stopBrushTransition() : playBrushTransition();
|
||||
});
|
||||
|
||||
dom.zooms.selectAll('text')
|
||||
.on('click', zoom => applyZoom(zoom));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates data displayed by this timeline, but only render if necessary
|
||||
* @param {Object} domain: Redux state domain subtree
|
||||
* @param {Object} app: Redux state app subtree
|
||||
*/
|
||||
function updateAxis(domain) {
|
||||
categoryGroups = domain.categoryGroups
|
||||
const groupStep = (106 - 30) / categoryGroups.length;
|
||||
let groupYs = Array.apply(null, Array(categoryGroups.length));
|
||||
groupYs = groupYs.map((g, i) => {
|
||||
return 30 + i * groupStep;
|
||||
});
|
||||
|
||||
scale.y = d3.scaleOrdinal()
|
||||
.domain(categoryGroups)
|
||||
.range(groupYs);
|
||||
|
||||
axis.y =
|
||||
d3.axisLeft(scale.y)
|
||||
.tickValues(categoryGroups);
|
||||
}
|
||||
|
||||
function update(domain, app) {
|
||||
updateAxis(domain);
|
||||
renderAxis();
|
||||
|
||||
events = domain.events;
|
||||
range = app.range;
|
||||
selected = app.selected.slice(0);
|
||||
updateTimeRange();
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderAxis();
|
||||
renderTimeControls();
|
||||
renderTimeLabels();
|
||||
|
||||
renderEvents();
|
||||
renderHighlight();
|
||||
}
|
||||
|
||||
return {
|
||||
update,
|
||||
render,
|
||||
};
|
||||
}
|
||||
88
src/reducers/app.js
Normal file
88
src/reducers/app.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import initial from '../store/initial.js';
|
||||
|
||||
import {
|
||||
UPDATE_HIGHLIGHTED,
|
||||
UPDATE_SELECTED,
|
||||
UPDATE_FILTERS,
|
||||
UPDATE_TIMERANGE,
|
||||
RESET_ALLFILTERS,
|
||||
TOGGLE_LANGUAGE,
|
||||
FETCH_ERROR,
|
||||
} from '../actions';
|
||||
|
||||
function updateHighlighted(appState, action) {
|
||||
return Object.assign({}, appState, {
|
||||
highlighted: action.highlighted
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelected(appState, action) {
|
||||
return Object.assign({}, appState, {
|
||||
selected: action.selected
|
||||
});
|
||||
}
|
||||
|
||||
function updateFilters(appState, action) { // XXX
|
||||
return Object.assign({}, appState, {
|
||||
filters: Object.assign({}, appState.filters, action.filters)
|
||||
});
|
||||
}
|
||||
|
||||
function updateTimeRange(appState, action) { // XXX
|
||||
return Object.assign({}, appState, {
|
||||
filters: Object.assign({}, appState.filters, action.range),
|
||||
});
|
||||
}
|
||||
|
||||
function resetAllFilters(appState) { // XXX
|
||||
return Object.assign({}, appState, {
|
||||
filters: Object.assign({}, appState.filters, {
|
||||
tags: [],
|
||||
categories: [],
|
||||
range: [
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2014-09-25T12:00:00"),
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2014-09-28T12:00:00")
|
||||
],
|
||||
}),
|
||||
selected: [],
|
||||
});
|
||||
}
|
||||
|
||||
function toggleLanguage(appState, action) {
|
||||
let otherLanguage = (appState.language === 'es-MX') ? 'en-US' : 'es-MX';
|
||||
return Object.assign({}, appState, {
|
||||
language: action.language || otherLanguage
|
||||
});
|
||||
}
|
||||
|
||||
function fetchError(state, action) {
|
||||
return {
|
||||
...state,
|
||||
error: action.message,
|
||||
notifications: [{ type: 'error', message: action.message }]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function app(appState = initial.app, action) {
|
||||
switch (action.type) {
|
||||
case UPDATE_HIGHLIGHTED:
|
||||
return updateHighlighted(appState, action);
|
||||
case UPDATE_SELECTED:
|
||||
return updateSelected(appState, action);
|
||||
case UPDATE_FILTERS:
|
||||
return updateFilters(appState, action);
|
||||
case UPDATE_TIMERANGE:
|
||||
return updateTimeRange(appState, action);
|
||||
case RESET_ALLFILTERS:
|
||||
return resetAllFilters(appState, action);
|
||||
case TOGGLE_LANGUAGE:
|
||||
return toggleLanguage(appState, action);
|
||||
case FETCH_ERROR:
|
||||
return fetchError(appState, action);
|
||||
default:
|
||||
return appState;
|
||||
}
|
||||
}
|
||||
|
||||
export default app;
|
||||
25
src/reducers/domain.js
Normal file
25
src/reducers/domain.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import initial from '../store/initial.js';
|
||||
|
||||
import {
|
||||
UPDATE_DOMAIN,
|
||||
} from '../actions';
|
||||
|
||||
import { parseDateTimes } from './utils/helpers.js';
|
||||
import { validate } from './utils/validators.js';
|
||||
|
||||
function updateDomain(domainState, action) {
|
||||
action.domain.events = parseDateTimes(action.domain.events);
|
||||
|
||||
return Object.assign({}, domainState, validate(action.domain));
|
||||
}
|
||||
|
||||
function domain(domainState = initial.domain, action) {
|
||||
switch (action.type) {
|
||||
case UPDATE_DOMAIN:
|
||||
return updateDomain(domainState, action);
|
||||
default:
|
||||
return domainState;
|
||||
}
|
||||
}
|
||||
|
||||
export default domain;
|
||||
13
src/reducers/index.js
Normal file
13
src/reducers/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import {
|
||||
combineReducers
|
||||
} from 'redux'
|
||||
|
||||
import domain from './domain.js'
|
||||
import app from './app.js'
|
||||
import ui from './ui.js'
|
||||
|
||||
export default combineReducers({
|
||||
app,
|
||||
domain,
|
||||
ui
|
||||
});;
|
||||
12
src/reducers/schema/categorySchema.js
Normal file
12
src/reducers/schema/categorySchema.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
const categorySchema = Joi.object().keys({
|
||||
category: Joi.string().required(),
|
||||
category_label: Joi.string().allow('').required(),
|
||||
group: Joi.string(),
|
||||
group_label: Joi.string(),
|
||||
});
|
||||
|
||||
const optionalSchema = categorySchema.optionalKeys('group', 'group_label');
|
||||
|
||||
export default categorySchema;
|
||||
20
src/reducers/schema/eventSchema.js
Normal file
20
src/reducers/schema/eventSchema.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
const eventSchema = Joi.object().keys({
|
||||
id: Joi.string().required(),
|
||||
description: Joi.string().allow('').required(),
|
||||
date: Joi.string().required(),
|
||||
time: Joi.string().required(),
|
||||
time_precision: Joi.string().allow(''),
|
||||
location: Joi.string().allow('').required(),
|
||||
latitude: Joi.string().required(),
|
||||
longitude: Joi.string().required(),
|
||||
type: Joi.string().allow(''),
|
||||
category: Joi.string().required(),
|
||||
source: Joi.string().allow(''),
|
||||
tags: Joi.string().allow(''),
|
||||
comments: Joi.string().allow(''),
|
||||
timestamp: Joi.string().required(),
|
||||
});
|
||||
|
||||
export default eventSchema;
|
||||
11
src/reducers/schema/siteSchema.js
Normal file
11
src/reducers/schema/siteSchema.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
const siteSchema = Joi.object().keys({
|
||||
id: Joi.string().required(),
|
||||
description: Joi.string().allow('').required(),
|
||||
site: Joi.string().required(),
|
||||
latitude: Joi.string().required(),
|
||||
longitude: Joi.string().required()
|
||||
});
|
||||
|
||||
export default siteSchema;
|
||||
104
src/reducers/ui.js
Normal file
104
src/reducers/ui.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import initial from '../store/initial.js';
|
||||
|
||||
import {
|
||||
TOGGLE_FETCHING_DOMAIN,
|
||||
TOGGLE_FETCHING_EVENTS,
|
||||
TOGGLE_VIEW,
|
||||
TOGGLE_TIMELINE,
|
||||
OPEN_CABINET,
|
||||
CLOSE_CABINET,
|
||||
TOGGLE_INFOPOPUP,
|
||||
TOGGLE_NOTIFICATIONS
|
||||
} from '../actions'
|
||||
|
||||
function toggleFetchingDomain(uiState, action) {
|
||||
return Object.assign({}, uiState, {
|
||||
flags: Object.assign({}, uiState.flags, {
|
||||
isFetchingDomain: !uiState.flags.isFetchingDomain
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function toggleFetchingEvents(uiState, action) {
|
||||
return Object.assign({}, uiState, {
|
||||
flags: Object.assign({}, uiState.flags, {
|
||||
isFetchingEvents: !uiState.flags.isFetchingEvents
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function toggleView(uiState, action) {
|
||||
return Object.assign({}, uiState, {
|
||||
flags: Object.assign({}, uiState.flags, {
|
||||
isView2d: !uiState.flags.isView2d
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function toggleTimeline(uiState, action) {
|
||||
return Object.assign({}, uiState, {
|
||||
flags: Object.assign({}, uiState.flags, {
|
||||
isTimeline: !uiState.flags.isTimeline
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function closeCabinet(uiState, action) {
|
||||
return Object.assign({}, uiState, {
|
||||
flags: Object.assign({}, uiState.flags, {
|
||||
isCabinet: false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function openCabinet(uiState, action) {
|
||||
return Object.assign({}, uiState, {
|
||||
flags: Object.assign({}, uiState.flags, {
|
||||
isCabinet: true
|
||||
}),
|
||||
components: Object.assign({}, uiState.components, {
|
||||
cabinetFileTab: action.tabNum,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function toggleInfoPopup(uiState, action) {
|
||||
return Object.assign({}, uiState, {
|
||||
flags: Object.assign({}, uiState.flags, {
|
||||
isInfopopup: !uiState.flags.isInfopopup
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function toggleNotifications(uiState, action) {
|
||||
return Object.assign({}, uiState, {
|
||||
flags: Object.assign({}, uiState.flags, {
|
||||
isNotification: !uiState.flags.isNotification
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function ui(uiState = initial.ui, action) {
|
||||
switch (action.type) {
|
||||
case TOGGLE_FETCHING_DOMAIN:
|
||||
return toggleFetchingDomain(uiState, action);
|
||||
case TOGGLE_FETCHING_EVENTS:
|
||||
return toggleFetchingEvents(uiState, action);
|
||||
case TOGGLE_VIEW:
|
||||
return toggleView(uiState, action);
|
||||
case TOGGLE_TIMELINE:
|
||||
return toggleTimeline(uiState, action);
|
||||
case OPEN_CABINET:
|
||||
return openCabinet(uiState, action);
|
||||
case CLOSE_CABINET:
|
||||
return closeCabinet(uiState, action);
|
||||
case TOGGLE_INFOPOPUP:
|
||||
return toggleInfoPopup(uiState, action);
|
||||
case TOGGLE_NOTIFICATIONS:
|
||||
return toggleNotifications(uiState, action);
|
||||
default:
|
||||
return uiState;
|
||||
}
|
||||
}
|
||||
|
||||
export default ui;
|
||||
18
src/reducers/utils/helpers.js
Normal file
18
src/reducers/utils/helpers.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export function parseDateTimes(arrayToParse) {
|
||||
const parsedArray = [];
|
||||
|
||||
arrayToParse.forEach(item => {
|
||||
let incoming_datetime = `${item.date}T00:00`;
|
||||
if (item.time) incoming_datetime = `${item.date}T${item.time}`;
|
||||
const parser = d3.timeParse(process.env.INCOMING_DATETIME_FORMAT);
|
||||
item.timestamp = d3.timeFormat("%Y-%m-%dT%H:%M:%S")(parser(incoming_datetime));
|
||||
|
||||
parsedArray.push(item);
|
||||
});
|
||||
|
||||
return parsedArray;
|
||||
}
|
||||
|
||||
export function capitalize(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
75
src/reducers/utils/validators.js
Normal file
75
src/reducers/utils/validators.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
import eventSchema from '../schema/eventSchema.js';
|
||||
import categorySchema from '../schema/categorySchema.js';
|
||||
import siteSchema from '../schema/siteSchema.js';
|
||||
|
||||
import { capitalize } from './helpers.js';
|
||||
|
||||
/*
|
||||
* Create an error notification object
|
||||
* Types: ['error', 'warning', 'good', 'neural']
|
||||
*/
|
||||
function makeError(type, id, message) {
|
||||
return {
|
||||
type: 'error',
|
||||
id,
|
||||
message: `${type} ${id}: ${message}`
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Validate domain schema
|
||||
*/
|
||||
export function validate(domain) {
|
||||
const sanitizedDomain = {
|
||||
events: [],
|
||||
categories: [],
|
||||
sites: [],
|
||||
notifications: domain.notifications,
|
||||
tags: domain.tags
|
||||
}
|
||||
|
||||
const discardedDomain = {
|
||||
events: [],
|
||||
categories: [],
|
||||
sites: []
|
||||
}
|
||||
|
||||
function validateItem(item, domainClass, schema) {
|
||||
const result = Joi.validate(item, schema);
|
||||
if (result.error !== null) {
|
||||
const id = item.id || '-';
|
||||
const domainStr = capitalize(domainClass);
|
||||
const error = makeError(domainStr, id, result.error.message);
|
||||
|
||||
discardedDomain[domainClass].push(Object.assign(item, { error }));
|
||||
} else {
|
||||
sanitizedDomain[domainClass].push(item);
|
||||
}
|
||||
}
|
||||
|
||||
domain.events.forEach(event => {
|
||||
validateItem(event, 'events', eventSchema);
|
||||
});
|
||||
domain.categories.forEach(category => {
|
||||
validateItem(category, 'categories', categorySchema);
|
||||
});
|
||||
domain.sites.forEach(site => {
|
||||
validateItem(site, 'sites', siteSchema);
|
||||
});
|
||||
|
||||
// Message the number of failed items
|
||||
Object.keys(discardedDomain).forEach(disc => {
|
||||
const len = discardedDomain[disc].length;
|
||||
if (len) {
|
||||
sanitizedDomain.notifications.push({
|
||||
message: `${len} invalid ${disc} not displayed.`,
|
||||
items: discardedDomain[disc],
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
return sanitizedDomain;
|
||||
}
|
||||
21
src/scss/_animations.scss
Normal file
21
src/scss/_animations.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fallFadeIn {
|
||||
from {
|
||||
transform: translate(0, -40px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
114
src/scss/_burger.scss
Normal file
114
src/scss/_burger.scss
Normal file
@@ -0,0 +1,114 @@
|
||||
|
||||
// Burger transition
|
||||
.side-menu-burg {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
float: right;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
appearance: none;
|
||||
box-shadow: none;
|
||||
border-radius: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
height: 2px;
|
||||
background: $offwhite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
span::before,
|
||||
span::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: $offwhite;
|
||||
border-radius: 4px;
|
||||
content: "";
|
||||
transition-duration: 0.2s, 0.2s;
|
||||
transition-delay: 0.2s, 0s;
|
||||
}
|
||||
|
||||
span::before {
|
||||
transition-property: top, transform;
|
||||
top: -8px;
|
||||
}
|
||||
|
||||
span::after {
|
||||
transition-property: bottom, transform;
|
||||
bottom: -8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
span::before {
|
||||
top: -6px;
|
||||
}
|
||||
|
||||
span::after {
|
||||
bottom: -6px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
span {
|
||||
background: $midwhite;
|
||||
transform: rotate(45deg);
|
||||
transition-delay: 0s, 0.2s;
|
||||
}
|
||||
|
||||
span::before,
|
||||
span::after {
|
||||
background: $midwhite;
|
||||
transition-delay: 0s, 0.2s;
|
||||
}
|
||||
|
||||
span::before {
|
||||
top: 0;
|
||||
transform: rotate(0deg);
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
span::after {
|
||||
bottom: 0;
|
||||
transform: rotate(-90deg);
|
||||
-webkit-transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
span,
|
||||
span::before,
|
||||
span::after {
|
||||
transition: 0.2s ease;
|
||||
background: $offwhite;
|
||||
}
|
||||
}
|
||||
|
||||
&.over-white:hover {
|
||||
span,
|
||||
span:before,
|
||||
span:after {
|
||||
transition: 0.2s ease;
|
||||
background: $darkgrey;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.side-menu-burg:focus {
|
||||
outline: none;
|
||||
}
|
||||
15
src/scss/_colors.scss
Normal file
15
src/scss/_colors.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
$offwhite: #efefef;
|
||||
$midwhite: #a0a0a0;
|
||||
$darkwhite: darken($midwhite, 15%);
|
||||
$yellow: #ffd800;
|
||||
$red: rgb(233, 0, 19);
|
||||
$green: rgb(61, 241, 79);
|
||||
$midgrey: rgb(44, 44, 44);
|
||||
$darkgrey: #232323;
|
||||
$black: #000000;
|
||||
|
||||
$category_group00: #FF0000;
|
||||
$category_group01: #226b22;
|
||||
$category_group02: #671f6f;
|
||||
$category_group03: #0000bf;
|
||||
$category_group04: #d3ce2a;
|
||||
9
src/scss/_fonts.scss
Normal file
9
src/scss/_fonts.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
$xsmall: 10px;//0.7em;
|
||||
$small: 11px;//0.9em;
|
||||
$normal: 12px;//1em;
|
||||
$large: 14px;//1.1em;
|
||||
$xlarge: 16px;//1.2em;
|
||||
$xxlarge: 20px;//1.4em;
|
||||
$xxxlarge: 32px;
|
||||
$title: 36px;
|
||||
$cover-title: 68px;
|
||||
16
src/scss/_icons.scss
Normal file
16
src/scss/_icons.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.icon {
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 1em;
|
||||
stroke-width: 0;
|
||||
stroke: $offwhite;
|
||||
fill: $offwhite;
|
||||
transform: scale(1.4);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
stroke: $yellow;
|
||||
fill: $yellow;
|
||||
}
|
||||
}
|
||||
12
src/scss/_levels.scss
Normal file
12
src/scss/_levels.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
/* z-index levels */
|
||||
$final-level: 10000;
|
||||
$loading-overlay: 1000;
|
||||
$overheader: 100;
|
||||
$header: 10;
|
||||
$map-overlay2: 4;
|
||||
$map-overlay: 2;
|
||||
$map: 1;
|
||||
$scene: 1;
|
||||
$hidden: -1;
|
||||
$timeline: 3;
|
||||
$timeslider: 3;
|
||||
44
src/scss/_video.scss
Normal file
44
src/scss/_video.scss
Normal file
@@ -0,0 +1,44 @@
|
||||
.fullscreen-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
z-index: -100;
|
||||
}
|
||||
|
||||
.fullscreen-bg__video {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-filter: contrast(70%) brightness(70%) grayscale(30%); /* Webkit */
|
||||
filter: gray; /* IE6-9 */
|
||||
filter: contrast(70%) brightness(70%) grayscale(30%) /* W3C */
|
||||
}
|
||||
|
||||
@media (min-aspect-ratio: 16/9) {
|
||||
.fullscreen-bg__video {
|
||||
height: 300%;
|
||||
top: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 16/9) {
|
||||
.fullscreen-bg__video {
|
||||
width: 300%;
|
||||
left: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.fullscreen-bg {
|
||||
background: url('/static/archive/img/city.jpg') center center / cover no-repeat;
|
||||
}
|
||||
|
||||
.fullscreen-bg__video {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
827
src/scss/cabinet.scss
Normal file
827
src/scss/cabinet.scss
Normal file
@@ -0,0 +1,827 @@
|
||||
@import 'icons';
|
||||
@import 'video';
|
||||
|
||||
.cabinet-wrapper {
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
color: $offwhite;
|
||||
background-color: $black;
|
||||
font-family: 'Merriweather', serif;
|
||||
transition: opacity 0.5s ease 0.1s, z-index 0.1s ease 0s;
|
||||
opacity: 1;
|
||||
z-index: $final-level;
|
||||
|
||||
&.hidden {
|
||||
transition: opacity 0.5s ease 0s, z-index 0.1s ease 0.5s;
|
||||
opacity: 0;
|
||||
z-index: $hidden;
|
||||
}
|
||||
|
||||
&.show {
|
||||
z-index: $final-level;
|
||||
}
|
||||
|
||||
.top-action {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1;
|
||||
|
||||
button {
|
||||
font-family: 'Lato';
|
||||
font-size: $xlarge;
|
||||
text-align: left;
|
||||
text-transform: uppercase;
|
||||
background: none;
|
||||
border: none;
|
||||
color: $offwhite;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.1em;
|
||||
|
||||
.label {
|
||||
height: 28px;
|
||||
float: left;
|
||||
line-height: 28px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
svg {
|
||||
float: left;
|
||||
path, circle, polygon,
|
||||
polyline, line {
|
||||
stroke-width: 2px;
|
||||
transition: 0.2s ease;
|
||||
stroke: $offwhite;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cabinet-header {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
padding: 20px 0;
|
||||
background: $black;
|
||||
transition: 1s ease;
|
||||
text-transform: uppercase;
|
||||
font-family: 'Merriweather', serif;
|
||||
|
||||
.header-title {
|
||||
width: 280px;
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
font-size: $normal;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
transition: 2s ease;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
p:first-child {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
p {
|
||||
transition: 2s ease;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.share-sm {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.side-menu-file-cabinet {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logo-fa {
|
||||
background: url('/static/archive/img/logo-fa-square.png');
|
||||
margin: 0px 0 30px 20px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
background-size: 30px;
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 3s;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.cabinet-wrapper-files {
|
||||
display: block;
|
||||
background: $black;
|
||||
}
|
||||
|
||||
&.cabinet-wrapper-cover {
|
||||
background-color: $black;
|
||||
background-repeat:no-repeat;
|
||||
-webkit-background-size:cover;
|
||||
-moz-background-size:cover;
|
||||
-o-background-size:cover;
|
||||
background-size:cover;
|
||||
background-position:center;
|
||||
}
|
||||
}
|
||||
|
||||
.cabinet-cover {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 0.5s;
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
box-sizing: border-box;
|
||||
padding: 30px;
|
||||
border-radius: 2px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cabinet-cover-header {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
animation-name: fallFadeIn;
|
||||
animation-duration: 3s;
|
||||
|
||||
.main-title {
|
||||
.title {
|
||||
font-size: $cover-title;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.untertitle {
|
||||
font-size: $xxlarge;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
.cabinet-cover-content {
|
||||
width: 100%;
|
||||
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 3s;
|
||||
|
||||
.tile-row {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.tile {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: calc(50% - 20px);
|
||||
height: 90px;
|
||||
line-height: 90px;
|
||||
box-sizing: border-box;
|
||||
background: none;
|
||||
border: 1px solid $offwhite;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
font-size: $xxlarge;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: 1s ease;
|
||||
letter-spacing: 0.1em;
|
||||
|
||||
span {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: " ";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
z-index: 0;
|
||||
transition: 1s ease;
|
||||
background-color: rgba($black, 0.15);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transition: 1s ease;
|
||||
letter-spacing: 0.15em;
|
||||
&:before{
|
||||
transition: 1s ease;
|
||||
background-color: rgba($black, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
&.full {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
& * {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tile-content {
|
||||
margin: 0 auto;
|
||||
padding: 30px 0;
|
||||
width: 200px;
|
||||
|
||||
.label {
|
||||
height: 28px;
|
||||
float: left;
|
||||
line-height: 28px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
svg {
|
||||
float: left;
|
||||
path, circle, polygon,
|
||||
polyline, line {
|
||||
stroke-width: 2px;
|
||||
transition: 0.2s ease;
|
||||
stroke: $offwhite;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-action {
|
||||
width: 90px;
|
||||
margin: 0 auto;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
font-family: 'Lato';
|
||||
font-size: $small;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
transition: 0.2s ease;
|
||||
border-bottom: 2px solid rgba($offwhite, 0);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.15em;
|
||||
border-bottom: 2px solid $yellow;
|
||||
}
|
||||
}
|
||||
|
||||
.cabinet-files {
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 0.5s;
|
||||
|
||||
&.file-tab-list-off {
|
||||
ul.cabinet-file-tab-list {
|
||||
transition: 0.2s ease;
|
||||
left: -300px;
|
||||
}
|
||||
}
|
||||
|
||||
ul.cabinet-file-tab-list {
|
||||
width: 300px;
|
||||
position: fixed;
|
||||
float: left;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding-top: 160px;
|
||||
border-right: 1px solid $darkgrey;
|
||||
background: $black;
|
||||
transition: 0.2s ease;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
|
||||
li.cabinet-file-tab {
|
||||
width: calc(100% - 40px);
|
||||
text-align: left;
|
||||
border: 0;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Merriweather', serif;
|
||||
font-size: $large;
|
||||
font-weight: 100;
|
||||
border-bottom: 1px solid $darkgrey;
|
||||
transition: 0.2s ease;
|
||||
color: $offwhite;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
&.react-tabs__tab--selected {
|
||||
color: $yellow;
|
||||
|
||||
&:after {
|
||||
content: ' →';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
text-align: left;
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
button {
|
||||
margin: 20px;
|
||||
width: calc(100% - 40px);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
font-family: 'Lato';
|
||||
font-size: $large;
|
||||
text-align: left;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&.secondary-action button {
|
||||
padding: 20px;
|
||||
border: 1px solid $midwhite;
|
||||
text-align: center;
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
border-color: $offwhite;
|
||||
background: rgba($offwhite, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.language-toggle {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.react-tabs__tab-panel {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.cabinet-file-content {
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
box-sizing: border-box;
|
||||
padding: 140px 30px 60px 30px;
|
||||
border-radius: 2px;
|
||||
overflow: auto;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 1.5s;
|
||||
|
||||
h1 {
|
||||
font-size: $title;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.title-separator {
|
||||
margin: 0 auto;
|
||||
width: 100px;
|
||||
border-bottom: 4px solid $yellow;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
letter-spacing: normal;
|
||||
text-transform: capitalize;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.cabinet-body-text {
|
||||
padding: 10px 0;
|
||||
|
||||
.section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 20px 0;
|
||||
font-size: $xlarge;
|
||||
font-weight: 100;
|
||||
line-height: 24px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.highpoint {
|
||||
font-size: $xxxlarge;
|
||||
margin: 60px 0;
|
||||
border-left: 4px solid $yellow;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.primary-action {
|
||||
button {
|
||||
font-size: $large;
|
||||
height: 140px;
|
||||
line-height: 140px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 1px solid $offwhite;
|
||||
background-size: 100%;
|
||||
color: $offwhite;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.1em;
|
||||
|
||||
&:after {
|
||||
content: ' →';
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
}
|
||||
|
||||
&.only-on-mobile {
|
||||
display: none;
|
||||
|
||||
button:after {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&.ja {
|
||||
button { background-image: url("/static/archive/img/scene01.jpg"); }
|
||||
}
|
||||
&.pj {
|
||||
button { background-image: url("/static/archive/img/scene02.jpg"); }
|
||||
}
|
||||
&.st {
|
||||
button { background-image: url("/static/archive/img/scene03.jpg"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
min-width: 200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: $large;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.primary-action {
|
||||
width: 100%;
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 1s;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 5%;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 120px;
|
||||
margin: 20px auto;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
font-family: 'Lato';
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.document-action {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
clear: both;
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 10px auto;
|
||||
padding: 0;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
border: 0;
|
||||
outline: none;
|
||||
border-bottom: 4px solid rgba($yellow, 0);
|
||||
background: none;
|
||||
|
||||
a {
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: $offwhite;
|
||||
|
||||
&:after {
|
||||
content: '↓';
|
||||
}
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
border-bottom: 4px solid rgba($yellow, 1);
|
||||
color: $yellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* VIDEO ON LANDING
|
||||
*/
|
||||
|
||||
.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
background: $black;
|
||||
}
|
||||
.video {
|
||||
display: block;
|
||||
left: 0px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 56.25%; /* 56.25% = 16:9. set ratio */
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
-webkit-transform-origin: 50% 0;
|
||||
transform-origin: 50% 0;
|
||||
-webkit-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.video .wrapper {
|
||||
display: block;
|
||||
height: 300%;
|
||||
left: 0px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
-webkit-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.video iframe {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
* VIDEO ON SITE
|
||||
*/
|
||||
|
||||
.video-responsive{
|
||||
overflow:hidden;
|
||||
padding-bottom:56.25%;
|
||||
position:relative;
|
||||
height:0;
|
||||
}
|
||||
.video-responsive iframe{
|
||||
left:0;
|
||||
top:0;
|
||||
height:100%;
|
||||
width:100%;
|
||||
position:absolute;
|
||||
}
|
||||
|
||||
/*
|
||||
* Responsiveness
|
||||
*
|
||||
*/
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.cabinet-files .cabinet-file-content .cabinet-body-text .primary-action {
|
||||
&.not-on-mobile {
|
||||
display: none;
|
||||
}
|
||||
&.only-on-mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.cabinet-cover {
|
||||
.content {
|
||||
padding: 0;
|
||||
}
|
||||
.cabinet-cover-header {
|
||||
margin-bottom: 40px;
|
||||
|
||||
.main-title {
|
||||
.title {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
.untertitle {
|
||||
font-size: $normal;
|
||||
}
|
||||
}
|
||||
|
||||
.cabinet-cover-content .tile-row {
|
||||
padding: 0;
|
||||
|
||||
.tile {
|
||||
width: 60%;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
margin: 0 20%;
|
||||
margin-bottom: 10px;
|
||||
font-size: $normal;
|
||||
&:first-child {
|
||||
margin-right: 20%;
|
||||
}
|
||||
&.tile00 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cabinet-files {
|
||||
ul.cabinet-file-tab-list {
|
||||
width: 100%;
|
||||
|
||||
li.cabinet-file-tab {
|
||||
text-align: center;
|
||||
font-size: $normal;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
&.file-tab-list-off {
|
||||
ul.cabinet-file-tab-list {
|
||||
transition: 0.2s ease;
|
||||
left: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
.cabinet-file-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 100px 15px 40px 15px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
|
||||
.cabinet-body-text {
|
||||
padding: 10px 0;
|
||||
font-size: $xlarge;
|
||||
|
||||
p {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.top-action.not-on-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablets with very short heights */
|
||||
@media (max-height: 768px) and (min-width: 780px) {
|
||||
.cabinet-cover .content {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.cabinet-cover .cabinet-cover-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.main-title .title {
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.cabinet-cover .cabinet-cover-content .tile-row .tile {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-size: $xlarge;
|
||||
|
||||
.tile-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1350px) {
|
||||
.cabinet-files {
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 0.5s;
|
||||
|
||||
&.file-tab-list-off {
|
||||
ul.cabinet-file-tab-list {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.side-menu-file-cabinet {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------- Non-Retina Screens ----------- */
|
||||
@media screen
|
||||
and (min-device-width: 1200px)
|
||||
and (max-device-width: 1600px)
|
||||
and (-webkit-min-device-pixel-ratio: 1) {
|
||||
}
|
||||
|
||||
/* ----------- Retina Screens ----------- */
|
||||
@media screen
|
||||
and (min-device-width: 1200px)
|
||||
and (max-device-width: 1600px)
|
||||
and (-webkit-min-device-pixel-ratio: 2)
|
||||
and (min-resolution: 192dpi) {
|
||||
}
|
||||
190
src/scss/card.scss
Normal file
190
src/scss/card.scss
Normal file
@@ -0,0 +1,190 @@
|
||||
.event-card {
|
||||
box-sizing: border-box;
|
||||
margin: 1px 0 0 0;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
border-radius: 3px;
|
||||
transition: 0.2 ease;
|
||||
background: $offwhite;
|
||||
color: $darkgrey;
|
||||
box-shadow: 0 19px 19px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
font-size: $large;
|
||||
line-height: $xxlarge;
|
||||
transition: 0.2s ease;
|
||||
height: auto;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $yellow;
|
||||
}
|
||||
|
||||
.card-bottomhalf {
|
||||
transition: 0.4s ease;
|
||||
height: auto;
|
||||
|
||||
&.folded {
|
||||
transition: 0.4s ease;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-transform: normal;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.event-card-section {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-toggle p {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
.arrow-down {
|
||||
display: inline-block;
|
||||
transition: 0.2s ease;
|
||||
border: solid $darkwhite;
|
||||
border-width: 0 2px 2px 0;
|
||||
padding: 3px;
|
||||
transform: rotate(-135deg);
|
||||
-webkit-transform: rotate(-135deg);
|
||||
|
||||
&.folded {
|
||||
transition: 0.2s ease;
|
||||
transform: rotate(45deg);
|
||||
-webkit-transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .arrow-down {
|
||||
transition: 0.2s ease;
|
||||
border: solid $darkgrey;
|
||||
border-width: 0 2px 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: $red;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
margin-top: 0;
|
||||
|
||||
.estimated-timestamp {
|
||||
color: $midwhite;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.category {
|
||||
.color-category {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
margin: 0px 5px 0 0;
|
||||
|
||||
&.category_group00 { background: $category_group00; }
|
||||
&.category_group01 { background: $category_group01; }
|
||||
&.category_group02 { background: $category_group02; }
|
||||
&.category_group03 { background: $category_group03; }
|
||||
&.category_group04 { background: $category_group04; }
|
||||
}
|
||||
}
|
||||
|
||||
.event-type {
|
||||
margin: 0 0 10px 0;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
margin: 0 5px 2px 0;
|
||||
color: $darkgrey;
|
||||
|
||||
&.flagged {
|
||||
background: $red;
|
||||
color: $offwhite;
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.location {
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.summary {
|
||||
overflow: auto;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/*
|
||||
https://github.com/tobiasahlin/SpinKit/blob/master/LICENSE
|
||||
*/
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
position: relative;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.double-bounce1, .double-bounce2 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: $darkgrey;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
-webkit-animation: sk-bounce 3.0s infinite ease-in-out;
|
||||
animation: sk-bounce 3.0s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.double-bounce2 {
|
||||
-webkit-animation-delay: -1.0s;
|
||||
animation-delay: -1.0s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-bounce {
|
||||
0%, 100% { -webkit-transform: scale(0.0) }
|
||||
50% { -webkit-transform: scale(1.0) }
|
||||
}
|
||||
|
||||
@keyframes sk-bounce {
|
||||
0%, 100% {
|
||||
transform: scale(0.0);
|
||||
-webkit-transform: scale(0.0);
|
||||
} 50% {
|
||||
transform: scale(1.0);
|
||||
-webkit-transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/scss/cardstack.scss
Normal file
92
src/scss/cardstack.scss
Normal file
@@ -0,0 +1,92 @@
|
||||
@import 'burger';
|
||||
@import 'card';
|
||||
|
||||
.card-stack {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
max-height: calc(100% - 208px);
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
z-index: $header;
|
||||
color: white;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&.full-height {
|
||||
max-height: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.card-stack-header {
|
||||
min-height: 38px;
|
||||
line-height: 38px;
|
||||
width: 360px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 5px;
|
||||
background: $black;
|
||||
border-radius: 2px;
|
||||
border: 1px solid $black;
|
||||
font-size: $large;
|
||||
transition: 0.2s ease;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.header-copy {
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
line-height: 20px;
|
||||
text-align: right;
|
||||
|
||||
&.top {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.side-menu-burg {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 9px;
|
||||
|
||||
span {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-stack-content {
|
||||
width: 360px;
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.folded {
|
||||
.card-stack-header {
|
||||
border: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-stack-content {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
}
|
||||
137
src/scss/common.scss
Normal file
137
src/scss/common.scss
Normal file
@@ -0,0 +1,137 @@
|
||||
@import 'colors';
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: $black;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $yellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Merriweather', serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.login-wrapper {
|
||||
margin-left: 20px;
|
||||
color: white;
|
||||
|
||||
.login-title {
|
||||
p.message {
|
||||
color: $yellow;
|
||||
}
|
||||
}
|
||||
|
||||
form span {
|
||||
width: 120px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
form input {
|
||||
margin: 10px 0;
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 5px;
|
||||
outline: none;
|
||||
font-family: 'Lato', sans-serif;
|
||||
|
||||
&:focus {
|
||||
border: 3px solid $yellow;
|
||||
}
|
||||
}
|
||||
|
||||
form button {
|
||||
background: $black;
|
||||
color: white;
|
||||
width: 120px;
|
||||
height: 30px;
|
||||
border: 1px solid $offwhite;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
margin-top: 10px;
|
||||
margin-left: 320px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
transition: 0.2s ease;
|
||||
border: 1px solid $yellow;
|
||||
color: $yellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
font-family: 'Lato', Helvetica sans-serif;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
::-moz-selection {
|
||||
color: $black;
|
||||
background: $yellow;
|
||||
}
|
||||
::selection {
|
||||
color: $black;
|
||||
background: $yellow;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
background: #000010;
|
||||
}
|
||||
|
||||
.primary-action {
|
||||
button {
|
||||
font-size: 1.2em;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
width: 200px;
|
||||
padding: 0;
|
||||
border: 1px solid $offwhite;
|
||||
background: none;
|
||||
color: $offwhite;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
color: $yellow;
|
||||
border: 1px solid $yellow;
|
||||
background: rgba(white, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Scrollbar
|
||||
*/
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
background: $offwhite;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
62
src/scss/header.scss
Normal file
62
src/scss/header.scss
Normal file
@@ -0,0 +1,62 @@
|
||||
.header {
|
||||
background: #000000;
|
||||
position: fixed;
|
||||
padding: 10px;
|
||||
z-index: 10;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
height: 40px;
|
||||
width: 240px;
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
cursor: pointer;
|
||||
|
||||
.header-title {
|
||||
a {
|
||||
font-family: 'Lato', Helvetica, serif;
|
||||
color: darken($offwhite, 5%);
|
||||
font-size: $xlarge;
|
||||
letter-spacing: 0.1em;
|
||||
float: left;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.side-menu-burg {
|
||||
right: 10px;
|
||||
span,
|
||||
span::before,
|
||||
span::after {
|
||||
background: $midwhite;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.side-menu-burg {
|
||||
span {
|
||||
transition: 0.2s ease;
|
||||
background: $offwhite;
|
||||
}
|
||||
span::before {
|
||||
transition: 0.2s ease;
|
||||
top: -6px;
|
||||
background: $offwhite;
|
||||
}
|
||||
|
||||
span::after {
|
||||
transition: 0.2s ease;
|
||||
bottom: -6px;
|
||||
background: $offwhite;
|
||||
}
|
||||
}
|
||||
.header-title a {
|
||||
transition: 0.2s ease;
|
||||
color: $offwhite;
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/scss/infopopup.scss
Normal file
116
src/scss/infopopup.scss
Normal file
@@ -0,0 +1,116 @@
|
||||
@import 'burger';
|
||||
|
||||
.infopopup {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
box-shadow: 10px -10px 38px rgba(0, 0, 0, 0.3), 10px 15px 12px rgba(0, 0, 0, 0.22);
|
||||
color: $darkgrey;
|
||||
position: absolute;
|
||||
background: $offwhite;
|
||||
border-radius: 5px;
|
||||
bottom: 20px;
|
||||
left: 100px;
|
||||
border: 3px solid $offwhite;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Lato', 'Helvetica', sans-serif;
|
||||
font-size: $large;
|
||||
transition: opacity 0.5s ease 0.1s, z-index 0.1s ease 0s;
|
||||
opacity: 1;
|
||||
z-index: $overheader;
|
||||
|
||||
&.hidden {
|
||||
transition: 0.5s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.legend-section {
|
||||
width: 300px;
|
||||
padding-left: 60px;
|
||||
height: 40px;
|
||||
display: inline-block;
|
||||
|
||||
svg {
|
||||
width: 100px;
|
||||
float: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-labels {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
width: calc(100% - 100px);
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: $xsmall;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
.color-category {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 10px;
|
||||
display: inline-block;
|
||||
margin: 0px 5px 0 0;
|
||||
|
||||
&.category_group00 { background: $category_group00; }
|
||||
&.category_group01 { background: $category_group01; }
|
||||
&.category_group02 { background: $category_group02; }
|
||||
&.category_group03 { background: $category_group03; }
|
||||
&.category_group04 { background: $category_group04; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.legend-labels .label {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.side-menu-burg {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
margin-bottom: 3px;
|
||||
padding-left: 80px;
|
||||
|
||||
.item-label {
|
||||
line-height: 15px;
|
||||
height: 15px;
|
||||
font-size: $normal;
|
||||
}
|
||||
|
||||
.color-marker {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
float: left;
|
||||
margin: 0 10px 0 0;
|
||||
border-radius: 15px;
|
||||
|
||||
&.victims { background-color: #C90500; }
|
||||
&.military { background-color: #319C31; }
|
||||
&.nonstate { background-color: #AC28AC; }
|
||||
&.state-police { background-color: #0000BF; }
|
||||
&.iguala-municipal-police { background-color: #00558D; }
|
||||
&.federal-police { background-color: #5756A2; }
|
||||
&.huitzuco-municipal-police { background-color: #4ECAC1; }
|
||||
&.cocula-municipal-police { background-color: #095959; }
|
||||
&.ambulance { background-color: #ffffff; }
|
||||
&.other { background-color: #D3CE2A; }
|
||||
&.drivers { background-color: #822519; }
|
||||
&.communications { background-color: #a6a6a6; }
|
||||
&.GIEI { background-color: #ffffff; }
|
||||
&.PGR { background-color: #000000; }
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/scss/loading.scss
Normal file
80
src/scss/loading.scss
Normal file
@@ -0,0 +1,80 @@
|
||||
.loading-overlay {
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
font-weight: 300;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: rgba(0,0,0,0.9);
|
||||
transition: 0.4s ease;
|
||||
z-index: $loading-overlay;
|
||||
opacity: 1;
|
||||
|
||||
.loading-wrapper {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 40%;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin: 0 0 0 -50%;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
|
||||
span {
|
||||
color: $offwhite;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
transition: opacity 0.4s ease, z-index .1s 0.4s;
|
||||
opacity: 0;
|
||||
z-index: $hidden;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
https://github.com/tobiasahlin/SpinKit/blob/master/LICENSE
|
||||
*/
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
position: relative;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.double-bounce1, .double-bounce2 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: $offwhite;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
-webkit-animation: sk-bounce 3.0s infinite ease-in-out;
|
||||
animation: sk-bounce 3.0s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.double-bounce2 {
|
||||
-webkit-animation-delay: -1.0s;
|
||||
animation-delay: -1.0s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-bounce {
|
||||
0%, 100% { -webkit-transform: scale(0.0) }
|
||||
50% { -webkit-transform: scale(1.0) }
|
||||
}
|
||||
|
||||
@keyframes sk-bounce {
|
||||
0%, 100% {
|
||||
transform: scale(0.0);
|
||||
-webkit-transform: scale(0.0);
|
||||
} 50% {
|
||||
transform: scale(1.0);
|
||||
-webkit-transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/scss/main.scss
Normal file
15
src/scss/main.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
@import 'colors';
|
||||
@import 'fonts';
|
||||
@import 'levels';
|
||||
@import 'animations';
|
||||
@import 'common';
|
||||
@import 'loading';
|
||||
@import 'header';
|
||||
@import 'cardstack';
|
||||
@import 'map';
|
||||
@import 'timeline';
|
||||
@import 'tag-filters';
|
||||
@import 'toolbar';
|
||||
@import 'infopopup';
|
||||
@import 'notification';
|
||||
@import 'scene';
|
||||
223
src/scss/map.scss
Normal file
223
src/scss/map.scss
Normal file
@@ -0,0 +1,223 @@
|
||||
@import 'popup';
|
||||
|
||||
@-webkit-keyframes pulsate {
|
||||
0% { opacity: 0.1; }
|
||||
50% { opacity: 0.25; }
|
||||
100% { opacity: 0.1; }
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 110px;
|
||||
right: 0;
|
||||
|
||||
.leaflet-container {
|
||||
height: 100%;
|
||||
|
||||
img.leaflet-tile {
|
||||
-webkit-filter: contrast(120%) brightness(115%) grayscale(95%); /* Webkit */
|
||||
filter: gray; /* IE6-9 */
|
||||
filter: contrast(120%) brightness(115%) grayscale(95%); /* W3C */
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
z-index: $hidden;
|
||||
}
|
||||
&.show {
|
||||
z-index: $map;
|
||||
}
|
||||
.event {
|
||||
fill: $yellow;
|
||||
cursor: pointer;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.link {
|
||||
stroke: $midgrey;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 2px 5px;
|
||||
}
|
||||
|
||||
.site-label {
|
||||
background: rgba($black,0.6);
|
||||
color: #fff;
|
||||
padding: 2px 7px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
border: rgba($black,0.6);
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
&::before {
|
||||
border-top-color: rgba($black,0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Leaflet mapping controls
|
||||
*/
|
||||
.leaflet-touch .leaflet-bar {
|
||||
.leaflet-control-zoom {
|
||||
border: 0;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
a.leaflet-control-zoom-in,
|
||||
a.leaflet-control-zoom-out {
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
a.leaflet-control-zoom-in {
|
||||
border-bottom: 1px solid $yellow;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Leaflet marker and popups
|
||||
*/
|
||||
|
||||
.leaflet-svg {
|
||||
display: block;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-popup {
|
||||
display: none;
|
||||
|
||||
&.do-display {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 3px;
|
||||
background: $black;
|
||||
|
||||
.leaflet-popup-content {
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 3px 5px;
|
||||
|
||||
.event-card {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-popup-close-button {
|
||||
display: none;
|
||||
|
||||
& + .leaflet-popup-content-wrapper .leaflet-popup-content {
|
||||
padding-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-popup-tip-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.leaflet-pane > svg path.bus-route,
|
||||
.leaflet-pane > svg path.district {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.eventLocationMarker {
|
||||
fill: none;
|
||||
stroke: $yellow;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* Elements
|
||||
*/
|
||||
|
||||
.location-event-marker {
|
||||
stroke-width: 0;
|
||||
transition: 0.2s ease;
|
||||
fill-opacity: 0.8;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
fill-opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.coevent-marker {
|
||||
fill-opacity: 0.1;
|
||||
stroke-dasharray: 8px 4px;
|
||||
stroke-width: 2px;
|
||||
opacity: 1;
|
||||
}
|
||||
.coevent-path {
|
||||
stroke-dasharray: 8px 4px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.district-boundaries {
|
||||
fill: $red;
|
||||
fill-opacity: 0.3;
|
||||
stroke-width: 2;
|
||||
stroke: $red;
|
||||
}
|
||||
|
||||
.path-polyline {
|
||||
stroke: $darkgrey;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.route-polyline {
|
||||
transition: 0.2s ease;
|
||||
stroke: $darkgrey;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
stroke: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.district-popup {
|
||||
button {
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
width: 200px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
background-size: 100%;
|
||||
color: $offwhite;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
|
||||
p {
|
||||
font-size: $normal;
|
||||
margin: 0;
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.1em;
|
||||
&:first-child {
|
||||
font-size: $xsmall;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
p:last-child {
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/scss/notification.scss
Normal file
71
src/scss/notification.scss
Normal file
@@ -0,0 +1,71 @@
|
||||
@import 'burger';
|
||||
|
||||
.notification-wrapper {
|
||||
top: 60px;
|
||||
right: 60px;
|
||||
width: 400px;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.notification {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
box-shadow: 10px -10px 38px rgba(0, 0, 0, 0.3), 10px 15px 12px rgba(0, 0, 0, 0.22);
|
||||
color: $darkgrey;
|
||||
background: $offwhite;
|
||||
border-radius: 5px;
|
||||
border: 3px solid $offwhite;
|
||||
padding: 20px;
|
||||
margin-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Lato', 'Helvetica', sans-serif;
|
||||
font-size: $large;
|
||||
transition: opacity 0.5s ease 0.1s, z-index 0.1s ease 0s;
|
||||
opacity: 1;
|
||||
z-index: $overheader;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: lighten($offwhite, 5%);
|
||||
transition: background-color 0.4s;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
transition: 0.5s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.side-menu-burg {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: inline-block;
|
||||
|
||||
&.error { color: red; }
|
||||
&.warning { color: orange; }
|
||||
&.good { color: green; }
|
||||
&.neutral { color: $darkgrey; }
|
||||
}
|
||||
|
||||
.details {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.true {
|
||||
height: auto;
|
||||
transition: height 0.4s;
|
||||
}
|
||||
|
||||
&.false {
|
||||
height: 0;
|
||||
transition: height 0.4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/scss/popup.scss
Normal file
81
src/scss/popup.scss
Normal file
@@ -0,0 +1,81 @@
|
||||
.popup {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 15px;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
border-radius: 2px;
|
||||
transition: 0.2 ease;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
transition: 0.4s ease;
|
||||
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
|
||||
&:hover {
|
||||
transition: 0.4s ease;
|
||||
box-shadow: 0 29px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
.card-tophalf {
|
||||
height: 100px;
|
||||
|
||||
.left {
|
||||
float: left;
|
||||
width: 120px;
|
||||
padding-right: 5px;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px dotted $midwhite;
|
||||
}
|
||||
.right {
|
||||
float: left;
|
||||
width: 225px;
|
||||
padding-left: 5px;
|
||||
height: 90px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.tag,
|
||||
p.see-more {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $yellow;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-size: $xlarge;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.location {
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
font-size: $normal;
|
||||
color: $offwhite;
|
||||
}
|
||||
|
||||
.estimated-timestamp {
|
||||
margin-top: 3px;
|
||||
margin-left: 3px;
|
||||
font-size: $xsmall;
|
||||
color: $midwhite;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.summary {
|
||||
max-height: 200px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: scroll;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.source {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
177
src/scss/scene.scss
Normal file
177
src/scss/scene.scss
Normal file
@@ -0,0 +1,177 @@
|
||||
.scene-wrapper {
|
||||
#container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: block;
|
||||
}
|
||||
&.hidden {
|
||||
z-index: $hidden;
|
||||
}
|
||||
&.show {
|
||||
z-index: $scene;
|
||||
}
|
||||
}
|
||||
#loadingText {
|
||||
text-align: center;
|
||||
position:relative;
|
||||
margin: 0 auto;
|
||||
margin-top: 20px;
|
||||
clear:left;
|
||||
height:auto;
|
||||
z-index: 0;
|
||||
color: rgba( 255, 255, 255, 255 );
|
||||
font-size: $normal;
|
||||
font-weight: 700;
|
||||
color: $offwhite;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
||||
.back-to-map {
|
||||
button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 130px;
|
||||
height: 20px;
|
||||
width: 250px;
|
||||
text-align: left;
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin-bottom: 10px;
|
||||
border: none;
|
||||
background-size: 100%;
|
||||
line-height: 20px;
|
||||
color: $offwhite;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.1em;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
KEYFRAME INFO
|
||||
*/
|
||||
.keyframe-info {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
left: 130px;
|
||||
height: auto;
|
||||
width: 270px;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
max-height: calc(100% - 250px);
|
||||
overflow: auto;
|
||||
box-shadow: 0 19px 38px rgba($black, 0.3), 0 15px 12px rgba($black, 0.22);
|
||||
background: $black;
|
||||
border: 1px solid $midgrey;
|
||||
color: $offwhite;
|
||||
font-family: 'Merriweather', 'Georgia', serif;
|
||||
|
||||
h3, h6 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $large;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: 'Lato', 'Helvetica', sans-serif;
|
||||
font-size: $normal;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
.action {
|
||||
width: calc(50% - 5px);
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
line-height: 40px;
|
||||
font-family: 'Lato', 'Helvetica', sans-serif;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
|
||||
&:not(.disabled) {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease;
|
||||
color: $yellow;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $midgrey;
|
||||
cursor: normal;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
DAT GUI
|
||||
*/
|
||||
.dg .a {
|
||||
margin-right: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.dg .cr.number {
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
input[type=text] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dg.main .close-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dg .c {
|
||||
width: 66%;
|
||||
.slider {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
height: 12px;
|
||||
|
||||
.slider-fg {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.slider-fg {
|
||||
background: $offwhite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dg .c .slider-fg {
|
||||
background: $midwhite;
|
||||
}
|
||||
|
||||
.dg .property-name {
|
||||
width: 33%;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
line-height: 20px;
|
||||
color: white;
|
||||
}
|
||||
47
src/scss/tabs.scss
Normal file
47
src/scss/tabs.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
.react-tabs {
|
||||
padding-top: 0;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
|
||||
[role=tablist] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
[role=tab] {
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
font-size: $xlarge;
|
||||
width: 33%;
|
||||
background: none;
|
||||
color: $midwhite;
|
||||
outline: none;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.4);
|
||||
list-style-type: none;
|
||||
box-sizing: border-box;
|
||||
&:hover {
|
||||
color: $offwhite;
|
||||
}
|
||||
}
|
||||
|
||||
[role=tab][aria-selected=true] {
|
||||
font-weight: 700;
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
color: $offwhite;
|
||||
border: 1px solid;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.react-innertabpanel {
|
||||
box-sizing: border-box;
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
85
src/scss/tag-filters.scss
Normal file
85
src/scss/tag-filters.scss
Normal file
@@ -0,0 +1,85 @@
|
||||
.applied-tagFilters {
|
||||
position: fixed;
|
||||
top: 135px;
|
||||
right: 5px;
|
||||
max-width: 260px;
|
||||
background: $black;
|
||||
color: $offwhite;
|
||||
padding: 10px;
|
||||
font-size: $small;
|
||||
z-index: $map-overlay;
|
||||
|
||||
.caption {
|
||||
font-size: $small;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.tag-chip-group {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-height: 150px;
|
||||
background: $midgrey;
|
||||
border-radius: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px 5px 5px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
|
||||
h3 {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.applied-tagFilter-chip {
|
||||
width: auto;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
color: $black;
|
||||
min-height: 16px;
|
||||
line-height: 16px;
|
||||
margin-top: 5px;
|
||||
margin-left: 5px;
|
||||
padding: 1px 5px;
|
||||
transition: 0.2s ease;
|
||||
background: $yellow;
|
||||
float: right;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
background: lighten($yellow, 15%);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
cursor: pointer;
|
||||
max-width: 350px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-top: -14px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
color: rgba($black, 0.25);
|
||||
fill: rgba($black, 0.25);
|
||||
transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
|
||||
|
||||
&:hover {
|
||||
color: rgba($black, 0.6);
|
||||
fill: rgba($black, 0.6);
|
||||
transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
288
src/scss/timeline.scss
Normal file
288
src/scss/timeline.scss
Normal file
@@ -0,0 +1,288 @@
|
||||
|
||||
.timeline-wrapper {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
left: 110px;
|
||||
right: 0px;
|
||||
height: 170px;
|
||||
background: rgba($black, 0.8);
|
||||
box-shadow: 0 -10px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
color: white;
|
||||
transition: 0.2s ease;
|
||||
bottom: 0px;
|
||||
z-index: $timeline;
|
||||
|
||||
&.folded {
|
||||
transition: 0.2s ease;
|
||||
bottom: -170px;
|
||||
|
||||
.timeline-header .timeline-toggle p .arrow-down {
|
||||
transform: translate(0, 5px)rotate(-135deg);
|
||||
-webkit-transform: translate(0, 5px)rotate(-135deg);
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
font-size: $large;
|
||||
font-weight: 700;
|
||||
|
||||
.timeline-toggle {
|
||||
position: absolute;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
width: 60px;
|
||||
height: 25px;
|
||||
margin: 0 auto;
|
||||
background: rgba($black, 0.8);
|
||||
margin-top: -25px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.arrow-down {
|
||||
transition: 0.2s ease;
|
||||
border-right: 2px solid $offwhite;
|
||||
border-bottom: 2px solid $offwhite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
transition: 0.2s ease;
|
||||
transform: rotate(45deg);
|
||||
-webkit-transform: rotate(45deg);
|
||||
border-right: 2px solid $midwhite;
|
||||
border-bottom: 2px solid $midwhite;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-info {
|
||||
position: absolute;
|
||||
margin-top: -70px;
|
||||
margin-left: 10px;
|
||||
background: rgba($black, 0.8);
|
||||
padding: 10px;
|
||||
min-height: 20px;
|
||||
p {
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
|
||||
&:first-child {
|
||||
text-transform: none;
|
||||
font-size: $normal;
|
||||
letter-spacing: 0.05;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
height: 160px;
|
||||
padding-top: 20px;
|
||||
|
||||
.timeline-labels {
|
||||
padding-top: 2px;
|
||||
padding-left: 20px;
|
||||
margin-right: 0px;
|
||||
border-right: 1px solid $midgrey;
|
||||
width: 175px;
|
||||
height: 180px;
|
||||
float: left;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
|
||||
.timeline-label-title {
|
||||
font-size: $normal;
|
||||
font-weight: 700;
|
||||
fill: $offwhite;
|
||||
letter-spacing: 0.1em;
|
||||
height: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.timeline-label {
|
||||
font-size: $small;
|
||||
line-height: 16px;
|
||||
color: $offwhite;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
}
|
||||
|
||||
.timeLabel {
|
||||
font-size: $normal;
|
||||
fill: $midwhite;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
/*width: calc(100% - 200px);*/
|
||||
width: calc(100% - 40px);
|
||||
margin-left: 20px;
|
||||
box-sizing: border-box;
|
||||
float: left;
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.domain {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tick {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
line {
|
||||
stroke: rgb(199, 199, 199);
|
||||
shape-rendering: crispEdges;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
text {
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
fill: $midwhite;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.xAxis {
|
||||
line {
|
||||
stroke-dasharray: 1px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.yAxis {
|
||||
.tick line {
|
||||
stroke-width: 15px;
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
}
|
||||
}
|
||||
|
||||
.axisBoundaries {
|
||||
stroke: $offwhite;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 1px 4px;
|
||||
}
|
||||
|
||||
.event {
|
||||
cursor: pointer;
|
||||
opacity: .7;
|
||||
|
||||
&.mouseover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
fill: none;
|
||||
stroke: $offwhite;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 5px 2px;
|
||||
}
|
||||
|
||||
.coevent {
|
||||
opacity: .7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.time-controls path,
|
||||
.time-controls rect {
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease;
|
||||
fill: $midwhite;
|
||||
|
||||
&:hover path,
|
||||
&:hover path, {
|
||||
transition: 0.2s ease;
|
||||
fill: $offwhite;
|
||||
}
|
||||
}
|
||||
|
||||
.time-controls-inline path {
|
||||
cursor: pointer;
|
||||
fill: $offwhite;
|
||||
}
|
||||
|
||||
.time-controls circle,
|
||||
.time-controls-inline circle {
|
||||
fill: $midwhite;
|
||||
fill-opacity: 0.01;
|
||||
cursor: pointer;
|
||||
stroke: $midwhite;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.time-controls-inline circle { stroke: none; }
|
||||
|
||||
.time-controls g,
|
||||
.time-controls-inline {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
circle {
|
||||
transition: 0.2s ease;
|
||||
fill-opacity: 0.2;
|
||||
fill: $offwhite;
|
||||
}
|
||||
path,
|
||||
rect {
|
||||
transition: 0.2s ease;
|
||||
fill: $offwhite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zoom-level-button {
|
||||
font-size: $xsmall;
|
||||
cursor: pointer;
|
||||
text-anchor: middle;
|
||||
letter-spacing: 0.05em;
|
||||
transition: 0.2s ease;
|
||||
fill: $midwhite;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
transition: 0.2s ease;
|
||||
fill: $offwhite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Slider
|
||||
* https://bl.ocks.org/mbostock/6452972
|
||||
*/
|
||||
.track,
|
||||
.track-overlay {
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.track {
|
||||
stroke: $offwhite;
|
||||
stroke-opacity: 1;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.track-overlay {
|
||||
pointer-events: stroke;
|
||||
stroke-width: 15px;
|
||||
stroke: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.handle {
|
||||
fill: $offwhite;
|
||||
}
|
||||
660
src/scss/toolbar.scss
Normal file
660
src/scss/toolbar.scss
Normal file
@@ -0,0 +1,660 @@
|
||||
@import 'burger';
|
||||
@import 'tabs';
|
||||
|
||||
.toolbar-wrapper {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
z-index: $header;
|
||||
background: $midgrey;
|
||||
|
||||
.toolbar {
|
||||
position: relative;
|
||||
width: 110px;
|
||||
height: 100%;
|
||||
padding: 20px 0px;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
color: $offwhite;
|
||||
background: $darkgrey;
|
||||
text-align: center;
|
||||
font-size: $normal;
|
||||
font-weight: 100;
|
||||
transition: 0.2s ease;
|
||||
z-index: $header;
|
||||
|
||||
.toolbar-header {
|
||||
margin: 0 15px 10px 15px;
|
||||
padding: 10px 0 25px 0;
|
||||
transition: 0.2s ease;
|
||||
border-bottom: 2px solid $midwhite;
|
||||
text-transform: uppercase;
|
||||
font-family: 'Merriweather', serif;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
font-size: $normal;
|
||||
margin: 0;
|
||||
}
|
||||
p:first-child {
|
||||
font-size: $xsmall;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
border-bottom: 2px solid $offwhite;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-tabs {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
position: absolute;
|
||||
width: 110px;
|
||||
bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.bottom-action-block {
|
||||
display: block;
|
||||
|
||||
&:last-child {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 60px;
|
||||
height: 25px;
|
||||
border-radius: 30px;
|
||||
background: none;
|
||||
margin: 0 auto;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
outline: none;
|
||||
font-family: 'Lato';
|
||||
font-size: $xsmall;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease;
|
||||
border: 1px solid $midwhite;
|
||||
color: $midwhite;
|
||||
|
||||
&.tiny {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
margin-right: 2px;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
transition: 0.2s ease;
|
||||
border: 1px solid $offwhite;
|
||||
color: $offwhite;
|
||||
|
||||
svg path { stroke: $offwhite; }
|
||||
svg polyline { stroke: $offwhite; }
|
||||
svg polygon { fill: $offwhite; }
|
||||
}
|
||||
|
||||
svg {
|
||||
|
||||
&.reset {
|
||||
margin-left: -4px;
|
||||
margin-top: -1px;
|
||||
-webkit-transform: scale(0.9);
|
||||
-moz-transform: translate(-2px,1px)scale(0.9);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
path, polyline {
|
||||
fill: none;
|
||||
stroke: $midwhite;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
polygon {
|
||||
fill: $midwhite;
|
||||
}
|
||||
|
||||
&.coevents {
|
||||
margin: 0;
|
||||
-webkit-transform: scale(0.9);
|
||||
transform: scale(1.2);
|
||||
|
||||
path { stroke-width: 2px; }
|
||||
rect {
|
||||
fill: $midwhite;
|
||||
&.no-fill { fill: $darkgrey; }
|
||||
}
|
||||
line {
|
||||
stroke-width: 1px;
|
||||
stroke: $midwhite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
font-size: $xxlarge;
|
||||
bottom: 120px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border: 1px solid $offwhite;
|
||||
color: $offwhite;
|
||||
|
||||
svg path { stroke: $offwhite; }
|
||||
svg polyline { stroke: $offwhite; }
|
||||
svg polygon { fill: $offwhite; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-tab {
|
||||
display: inline-block;
|
||||
height: 60px;
|
||||
width: 110px;
|
||||
padding: 10px 0 5px 0;
|
||||
font-weight: 400;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease;
|
||||
color: $midwhite;
|
||||
|
||||
svg {
|
||||
transform: scale(0.7);
|
||||
path, circle, polygon,
|
||||
polyline, line {
|
||||
stroke-width: 2px;
|
||||
transition: 0.2s ease;
|
||||
stroke: $midwhite;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
&.scenes {
|
||||
path {
|
||||
transition: 0.2s ease;
|
||||
fill: $midwhite;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-caption {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: $xsmall;
|
||||
margin-top: -2px;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $black;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
transition: 0.2s ease;
|
||||
color: $offwhite;
|
||||
|
||||
svg {
|
||||
path, circle, polygon,
|
||||
polyline, line {
|
||||
transition: 0.2s ease;
|
||||
stroke: $offwhite;
|
||||
}
|
||||
|
||||
&.scenes {
|
||||
path {
|
||||
transition: 0.2s ease;
|
||||
fill: $offwhite;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-panels {
|
||||
width: 440px;
|
||||
top: 15px;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 30px 10px 10px 30px;
|
||||
font-size: $normal;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
background: $black;
|
||||
color: $offwhite;
|
||||
position: fixed;
|
||||
transition: 0.2s ease;
|
||||
left: 110px;
|
||||
box-shadow: 10px -10px 38px rgba(0, 0, 0, 0.3), 10px 15px 12px rgba(0, 0, 0, 0.22);
|
||||
|
||||
h2 {
|
||||
font-family: 'Merriweather', 'Georgia', 'serif';
|
||||
font-size: $large;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $normal;
|
||||
font-family: 'Merriweather', 'Georgia', 'serif';
|
||||
line-height: 1.4em;
|
||||
};
|
||||
|
||||
.panel-header {
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
float: right;
|
||||
margin-left: 20px;
|
||||
margin-right: -45px;
|
||||
height: 36px;
|
||||
padding-top: 5px;
|
||||
box-sizing: border-box;
|
||||
margin-top: 10px;
|
||||
border-radius: 3px;
|
||||
background: $black;
|
||||
padding: 8px 6px;
|
||||
cursor: pointer;
|
||||
|
||||
.caret {
|
||||
position: relative;
|
||||
transform: translate(8px, 5px)rotate(45deg);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
transition: 0.2s ease;
|
||||
border-left: 2px solid $midwhite;
|
||||
border-bottom: 2px solid $midwhite;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.caret {
|
||||
transition: 0.2s ease;
|
||||
border-left: 2px solid $offwhite;
|
||||
border-bottom: 2px solid $offwhite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.people-tab {
|
||||
width: 50%;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
font-size: $normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
|
||||
svg {
|
||||
transform: translate(-2px,0)scale(0.6);
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
stroke: $offwhite;
|
||||
}
|
||||
}
|
||||
|
||||
&.react-tabs__tab--selected {
|
||||
svg circle,
|
||||
svg path {
|
||||
stroke: $offwhite;
|
||||
}
|
||||
}
|
||||
|
||||
svg circle,
|
||||
svg path {
|
||||
transition: 0.2s ease;
|
||||
fill: none;
|
||||
stroke: $midwhite;
|
||||
stroke-width: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.react-tabs__tab-list {
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.react-tabs__tab-panel {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.react-tabs__tab-panel--selected {
|
||||
height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
margin-top: 0;
|
||||
|
||||
.react-tabs__tab-panel--selected {
|
||||
padding-top: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
.react-tabs .react-innertabpanel {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
height: auto;
|
||||
transition: 0.2s ease;
|
||||
height: calc(100% - 310px);
|
||||
}
|
||||
|
||||
&.folded {
|
||||
transition: 0.2s ease;
|
||||
left: -440px;
|
||||
|
||||
ul {
|
||||
height: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 1px solid;
|
||||
height: 60px;
|
||||
color: $offwhite;
|
||||
background: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
margin: 20px 0;
|
||||
padding: 5px 10px;
|
||||
font-size: 18px;
|
||||
font-family: 'Lato', sans-serif;
|
||||
letter-spacing: 0.1em;
|
||||
transition: 0.2s ease;
|
||||
border-color: $midwhite;
|
||||
text-align: center;
|
||||
|
||||
&:focus {
|
||||
transition: 0.2s ease;
|
||||
border-color: $offwhite;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
background: none;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
font-size: $large;
|
||||
|
||||
button {
|
||||
height: 36px;
|
||||
border: 1px transparent;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: $offwhite;
|
||||
outline: none;
|
||||
transition: 0.2s ease;
|
||||
padding: 0 10px;
|
||||
text-align: left;
|
||||
float: left;
|
||||
|
||||
.checkbox {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1px solid $offwhite;
|
||||
box-sizing: border-box;
|
||||
background: none;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
width: calc(100% - 40px);
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
float: left;
|
||||
font-size: $normal;
|
||||
font-family: 'Merriweather', 'Georgia', 'serif';
|
||||
color: $midwhite;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
color: $offwhite;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
span {
|
||||
color: $offwhite;
|
||||
}
|
||||
.checkbox {
|
||||
background: $offwhite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
line-height: 10px;
|
||||
padding: 10px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
color: $offwhite;
|
||||
transition: 0.4s ease;
|
||||
transform: rotate(0deg);
|
||||
|
||||
&:after {
|
||||
content: '▾';
|
||||
}
|
||||
|
||||
&.folded {
|
||||
transition: 0.4s ease;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.panel-action {
|
||||
button {
|
||||
font-size: 1.2em;
|
||||
height: 140px;
|
||||
line-height: 140px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 1px solid $offwhite;
|
||||
background-size: 100%;
|
||||
color: $offwhite;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.1em;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
button { background-image: url("/static/archive/img/scene01.jpg"); }
|
||||
}
|
||||
&:nth-child(2n) {
|
||||
button { background-image: url("/static/archive/img/scene02.jpg"); }
|
||||
}
|
||||
&:nth-child(3n) {
|
||||
button { background-image: url("/static/archive/img/scene03.jpg"); }
|
||||
}
|
||||
|
||||
&.back-to-map {
|
||||
button { background-image: url("/static/archive/img/map.jpg"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.taggroup-wrapper {
|
||||
margin-top: 30px;
|
||||
z-index: 10;
|
||||
border-bottom: none;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid rgba(white, 0);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transition: 0.1s ease;
|
||||
}
|
||||
|
||||
.collapsible-item {
|
||||
width: calc(100% - 32px);
|
||||
float: left;
|
||||
}
|
||||
|
||||
.taggroup-header {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-size: $large;
|
||||
|
||||
h2::first-letter {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.taggroup-content {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
padding-left: 10px;
|
||||
box-sizing: border-box;
|
||||
transition: 0.2s ease;
|
||||
|
||||
.tagsubgroup-wrapper {
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(white, 0.25);
|
||||
&:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.tagsubgroup-header {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.folded {
|
||||
.tagsubgroup-content {
|
||||
overflow: hidden;
|
||||
padding: 0 10px;
|
||||
transition: 0.2s ease;
|
||||
height: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
overflow: auto;
|
||||
min-height: 32px;
|
||||
height: auto;
|
||||
|
||||
span {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-filter {
|
||||
outline: none;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: $offwhite;
|
||||
margin-left: 20px;
|
||||
width: calc(100% - 20px);
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
font-size: $normal;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border: 1px solid $black;
|
||||
border-bottom: 1px solid rgba(white, 0.25);
|
||||
&:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid rgba(white, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.folded {
|
||||
.filter-list-content {
|
||||
padding: 0 10px;
|
||||
border-top: 0;
|
||||
transition: 0.2s ease;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-content {
|
||||
.item {
|
||||
overflow: auto;
|
||||
min-height: 32px;
|
||||
height: auto;
|
||||
border-bottom: 1px solid rgba(white, 0.25);
|
||||
|
||||
span {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.path-list {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.item {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 678px) {
|
||||
.toolbar-wrapper {
|
||||
.toolbar-tab {
|
||||
height: 60px;
|
||||
padding: 0;
|
||||
|
||||
.tab-caption {
|
||||
transition: 0.2s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.tab-caption {
|
||||
transition: 0.2s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar .bottom-actions {
|
||||
.action-button {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/scss/video.scss
Normal file
33
src/scss/video.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
.video-wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
width: 740px;
|
||||
height: 420px;
|
||||
transition: opacity 500ms;
|
||||
background-color: black;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-js .vjs-big-play-button {
|
||||
font-size: 3em;
|
||||
line-height: 40px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: none;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
border-radius: 20px;
|
||||
transition: 0.2s ease;
|
||||
border: 1px solid $midwhite;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
border: 1px solid $offwhite;
|
||||
}
|
||||
}
|
||||
134
src/selectors/index.js
Normal file
134
src/selectors/index.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
createSelector
|
||||
} from 'reselect'
|
||||
|
||||
// Input selectors
|
||||
export const getEvents = state => state.domain.events;
|
||||
export const getLocations = state => state.domain.locations;
|
||||
export const getCategories = state => state.domain.categories;
|
||||
export const getSites = (state) => {
|
||||
if (process.env.features.USE_SITES) return state.domain.sites;
|
||||
return [];
|
||||
}
|
||||
export const getTags = state => state.domain.tags;
|
||||
|
||||
export const getCategoriesFilter = state => state.app.filters.categories;
|
||||
export const getTagsFilter = state => state.app.filters.tags;
|
||||
export const getRangeFilter = state => state.app.filters.range;
|
||||
|
||||
// NB: should we stick with the default semantics and name these as selectors?
|
||||
// e.g. 'selectEvents', 'selectCoevents'.
|
||||
// Filter events
|
||||
function isTaggedIn(event, tagFilters) {
|
||||
if (event.tags) {
|
||||
const tagsArray = event.tags.split(",");
|
||||
const isTagged = tagsArray.some((tag) => {
|
||||
return tagFilters.find((tagFilter) => {
|
||||
return (tagFilter.key === tag && tagFilter.active);
|
||||
})
|
||||
});
|
||||
return isTagged;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Of all available events, selects those that fall within the time range,
|
||||
* and if TAGS are being used, select them if their tags are enabled
|
||||
*/
|
||||
export const getFilteredEvents = createSelector(
|
||||
[getEvents, getTagsFilter, getRangeFilter],
|
||||
(events, tagFilters, rangeFilter) => {
|
||||
|
||||
return events.reduce((acc, value) => {
|
||||
const noTags = (tagFilters.length === 0 || !process.env.features.USE_TAGS || tagFilters.every(t => !t.active));
|
||||
|
||||
const isTagged = (noTags) || isTaggedIn(value, tagFilters);
|
||||
|
||||
const isRange = (rangeFilter[0] < d3.timeParse("%Y-%m-%dT%H:%M:%S")(value.timestamp)) &&
|
||||
(d3.timeParse("%Y-%m-%dT%H:%M:%S")(value.timestamp) < rangeFilter[1]);
|
||||
|
||||
if (isRange && isTagged) {
|
||||
const event = Object.assign({}, value);
|
||||
acc[event.id] = event;
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
});
|
||||
|
||||
/**
|
||||
* Of all the filtered events, group them by location and return a list of
|
||||
* locations with at least one event in it, based on the time range and tags
|
||||
*/
|
||||
export const getFilteredLocations = createSelector(
|
||||
[getFilteredEvents],
|
||||
(events) => {
|
||||
|
||||
const filteredLocations = {};
|
||||
events.forEach(event => {
|
||||
const location = event.location;
|
||||
if (filteredLocations[location]) {
|
||||
filteredLocations[location].events.push(event);
|
||||
} else {
|
||||
filteredLocations[location] = {
|
||||
label: location,
|
||||
events: [event],
|
||||
latitude: event.latitude,
|
||||
longitude: event.longitude
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Make locations an array are remove if any are undefined
|
||||
return Object.values(filteredLocations).filter(item => item);
|
||||
});
|
||||
|
||||
// Filter categories
|
||||
export const getFilteredCategories = createSelector(
|
||||
[getCategories],
|
||||
(categories) => {
|
||||
|
||||
return Object.values(categories);
|
||||
});
|
||||
|
||||
/**
|
||||
* Return categories by group
|
||||
*/
|
||||
export const getCategoryGroups = createSelector(
|
||||
[getFilteredCategories],
|
||||
(categories) => {
|
||||
const groups = {};
|
||||
categories.forEach((t) => { if (t.group && !groups[t.group]) { groups[t.group] = t.group_label } });
|
||||
return Object.keys(groups).concat(['other']);
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Given a tree of tags, return those tags as a list, where each node has been
|
||||
* aware of its depth, and given an 'active' flag
|
||||
*/
|
||||
export const getTagFilters = createSelector(
|
||||
[getTags],
|
||||
(tags) => {
|
||||
const allTags = [];
|
||||
let depth = 0;
|
||||
function traverseNode(node, depth) {
|
||||
// do something to node
|
||||
node.active = (!node.hasOwnProperty('active')) ? false : node.active;
|
||||
node.depth = depth;
|
||||
allTags.push(node)
|
||||
depth = depth + 1;
|
||||
|
||||
if (Object.keys(node.children).length > 0) {
|
||||
Object.values(node.children).forEach((childNode) => {
|
||||
traverseNode(childNode, depth);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tags.key && tags.children) traverseNode(tags, depth)
|
||||
return allTags;
|
||||
}
|
||||
)
|
||||
16
src/store/index.js
Normal file
16
src/store/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
createStore,
|
||||
applyMiddleware,
|
||||
compose
|
||||
} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import rootReducer from '../reducers';
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
composeEnhancers(applyMiddleware(thunk))
|
||||
);
|
||||
|
||||
export default store;
|
||||
97
src/store/initial.js
Normal file
97
src/store/initial.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// TODO: annotate sections of this state.
|
||||
|
||||
// NB: why does this canvas document need to be created?
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||||
|
||||
const initial = {
|
||||
domain: {
|
||||
events: [],
|
||||
locations: [],
|
||||
|
||||
categories: [],
|
||||
sites: [],
|
||||
|
||||
// Tag tree
|
||||
tags: { },
|
||||
notifications: [],
|
||||
},
|
||||
app: {
|
||||
error: null,
|
||||
highlighted: null,
|
||||
selected: [],
|
||||
notifications: [],
|
||||
filters: {
|
||||
range: [
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2014-08-22T12:00:00"),
|
||||
d3.timeParse("%Y-%m-%dT%H:%M:%S")("2014-08-27T12:00:00")
|
||||
],
|
||||
tags: [],
|
||||
categories: [],
|
||||
views: {
|
||||
events: true,
|
||||
coevents: false,
|
||||
routes: false,
|
||||
sites: true
|
||||
},
|
||||
},
|
||||
base_uri: 'http://127.0.0.1:8000/', // Modify accordingly on production setup.
|
||||
isMobile: (/Mobi/.test(navigator.userAgent)),
|
||||
isWebGL: (gl && gl instanceof WebGLRenderingContext),
|
||||
language: 'en-US',
|
||||
mapAnchor: process.env.MAP_ANCHOR,
|
||||
features: {
|
||||
USE_TAGS: process.env.features.USE_TAGS,
|
||||
USE_SEARCH: process.env.features.USE_SEARCH
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
style: {
|
||||
colors: {
|
||||
WHITE: "#efefef",
|
||||
YELLOW: "#ffd800",
|
||||
MIDGREY: "rgb(44, 44, 44)",
|
||||
DARKGREY: "#232323",
|
||||
PINK: "#F28B50",//rgb(232, 9, 90)",
|
||||
ORANGE: "#F25835",//rgb(232, 9, 90)",
|
||||
RED: "rgb(233, 0, 19)",
|
||||
BLUE: "#F2DE79",//"rgb(48, 103 , 217)",
|
||||
GREEN: "#4FF2F2",//"rgb(0, 158, 86)",
|
||||
},
|
||||
groupColors: {
|
||||
category_group00: "#FF0000",
|
||||
category_group01: "#226b22",
|
||||
category_group02: "#671f6f",
|
||||
category_group03: "#0000bf",
|
||||
category_group04: "#d3ce2a",
|
||||
other: "#FF0000"
|
||||
},
|
||||
palette: d3.schemeCategory10,
|
||||
},
|
||||
dom: {
|
||||
timeline: "timeline",
|
||||
timeslider: "timeslider",
|
||||
map: "map"
|
||||
},
|
||||
flags: {
|
||||
isFetchingDomain: false,
|
||||
isFetchingEvents: false,
|
||||
isView2d: true,
|
||||
isTimeline: true,
|
||||
isToolbar: false,
|
||||
isCardstack: true,
|
||||
isInfopopup: false,
|
||||
isNotification: true
|
||||
},
|
||||
tools: {
|
||||
formatter: d3.timeFormat("%d %b, %H:%M"),
|
||||
formatterWithYear: d3.timeFormat("%d %b %Y, %H:%M"),
|
||||
parser: d3.timeParse("%Y-%m-%dT%H:%M:%S")
|
||||
},
|
||||
components: {
|
||||
toolbarTab: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default initial;
|
||||
83
webpack.config.js
Normal file
83
webpack.config.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const webpack = require('webpack');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const userConfig = require('./config');
|
||||
const devMode = process.env.NODE_ENV !== 'production';
|
||||
const path = require('path');
|
||||
|
||||
const APP_DIR = path.resolve(__dirname, './src');
|
||||
const BUILD_DIR = path.resolve(__dirname, './build');
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
index: `${APP_DIR}/index.jsx`,
|
||||
},
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.scss$/,
|
||||
include: `${APP_DIR}`,
|
||||
use: [
|
||||
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
]
|
||||
}, {
|
||||
test: /\.js(x)?$/,
|
||||
exclude: /node_modules/,
|
||||
include: `${APP_DIR}`,
|
||||
use: {
|
||||
loader: 'babel-loader'
|
||||
},
|
||||
}, {
|
||||
test: /\.(eot|svg|otf|ttf|woff|woff2)$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
node: {
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
dns: 'empty'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['*', '.js', ],
|
||||
},
|
||||
output: {
|
||||
path: BUILD_DIR,
|
||||
filename: 'js/[name].bundle.js',
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
'NODE_ENV': JSON.stringify('production'),
|
||||
'MAPBOX_TOKEN': JSON.stringify(userConfig.MAPBOX_TOKEN),
|
||||
'SERVER_ROOT': JSON.stringify(userConfig.SERVER_ROOT),
|
||||
'EVENT_EXT': JSON.stringify(userConfig.EVENT_EXT),
|
||||
'CATEGORY_EXT': JSON.stringify(userConfig.CATEGORY_EXT),
|
||||
'TAG_TREE_EXT': JSON.stringify(userConfig.TAG_TREE_EXT),
|
||||
'SITES_EXT': JSON.stringify(userConfig.SITES_EXT),
|
||||
'EVENT_DESC_ROOT': JSON.stringify(userConfig.EVENT_DESC_ROOT),
|
||||
'MAP_ANCHOR': JSON.stringify(userConfig.MAP_ANCHOR),
|
||||
'INCOMING_DATETIME_FORMAT': JSON.stringify(userConfig.INCOMING_DATETIME_FORMAT),
|
||||
'features': {
|
||||
'USE_TAGS': JSON.stringify(userConfig.features.USE_TAGS),
|
||||
'USE_SEARCH': JSON.stringify(userConfig.features.USE_SEARCH),
|
||||
'USE_SITES': JSON.stringify(userConfig.features.USE_SITES)
|
||||
}
|
||||
}
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: devMode ? '[name].css' : '[name].[hash].css',
|
||||
chunkFilename: devMode ? '[id].css' : '[id].[hash].css',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './index.html',
|
||||
})
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
Reference in New Issue
Block a user