├── .env ├── .dockerignore ├── src ├── scss │ ├── mediaplayer.scss │ ├── _icons.scss │ ├── main.scss │ ├── video.scss │ ├── button.scss │ ├── satelliteoverlaytoggle.scss │ ├── _video.scss │ ├── tabs.scss │ ├── header.scss │ ├── popup.scss │ ├── notification.scss │ ├── _variables.scss │ ├── loading.scss │ ├── search.scss │ ├── _burger.scss │ ├── infopopup.scss │ ├── cardstack.scss │ ├── common.scss │ ├── narrativecard.scss │ └── card.scss ├── assets │ ├── fa-logo.png │ ├── bellingcat-logo.png │ ├── placeholder-image.jpg │ ├── fonts │ │ └── timemapfont.woff │ ├── satelliteoverlaytoggle │ │ ├── map.png │ │ └── sat.png │ ├── checkbox.svg │ ├── arrowdown.svg │ └── close.svg ├── components │ ├── .DS_Store │ ├── App.js │ ├── atoms │ │ ├── StaticPage.js │ │ ├── Spinner.js │ │ ├── CoverIcon.js │ │ ├── InfoIcon.js │ │ ├── SitesIcon.js │ │ ├── NoSource.js │ │ ├── RouteIcon.js │ │ ├── Loading.js │ │ ├── RefreshIcon.js │ │ ├── Controls.js │ │ ├── Popup.js │ │ ├── Checkbox.js │ │ ├── Md.js │ │ ├── CoeventIcon.js │ │ ├── ColoredMarkers.js │ │ ├── Content.js │ │ └── Media.js │ ├── space │ │ ├── Space.js │ │ └── carto │ │ │ └── atoms │ │ │ ├── Sites.js │ │ │ ├── DefsMarkers.js │ │ │ ├── Regions.js │ │ │ ├── SatelliteOverlayToggle.js │ │ │ ├── SelectedEvents.js │ │ │ ├── __tests__ │ │ │ └── SatelliteOverlayToggle.spec.js │ │ │ ├── Events.js │ │ │ ├── Clusters.js │ │ │ └── Narratives.js │ ├── time │ │ ├── atoms │ │ │ ├── Clip.js │ │ │ ├── DatetimeDot.js │ │ │ ├── DatetimeSquare.js │ │ │ ├── DatetimeTriangle.js │ │ │ ├── DatetimePentagon.js │ │ │ ├── DatetimeStar.js │ │ │ ├── Project.js │ │ │ ├── Header.js │ │ │ ├── Labels.js │ │ │ ├── DatetimeBar.js │ │ │ ├── Handles.js │ │ │ ├── ZoomControls.js │ │ │ └── Markers.js │ │ ├── Axis.js │ │ └── Categories.js │ ├── controls │ │ ├── atoms │ │ │ ├── Caret.js │ │ │ ├── ToolbarButton.js │ │ │ ├── CustomField.js │ │ │ ├── NarrativeClose.js │ │ │ ├── NarrativeAdjust.js │ │ │ ├── Time.js │ │ │ ├── PanelTree.js │ │ │ ├── NarrativeCard.js │ │ │ ├── Text.js │ │ │ ├── Button.js │ │ │ ├── SearchRow.jsx │ │ │ ├── TelegramEmbed.js │ │ │ └── Media.js │ │ ├── CategoriesListPanel.js │ │ ├── ShapesListPanel.js │ │ ├── NarrativeControls.js │ │ ├── BottomActions.js │ │ ├── FullScreenToggle.js │ │ ├── FilterListPanel.js │ │ ├── Search.js │ │ └── CardStack.js │ ├── InfoPopup.js │ └── Notification.js ├── reducers │ ├── features.js │ ├── validate │ │ ├── regionSchema.js │ │ ├── shapeSchema.js │ │ ├── associationsSchema.js │ │ ├── siteSchema.js │ │ ├── sourceSchema.js │ │ └── eventSchema.js │ ├── index.js │ ├── ui.js │ ├── __tests__ │ │ └── ui.spec.js │ └── domain.js ├── setupTests.js ├── common │ ├── global.js │ ├── data │ │ └── es-MX.json │ └── constants.js ├── store │ └── index.js ├── test │ └── App.test.js ├── selectors │ └── helpers.js └── index.jsx ├── public ├── favicon.ico └── index.html ├── docs ├── example-timemap.png ├── mapbox.md ├── custom-covers.md └── configuration.md ├── .prettierignore ├── config ├── jest │ ├── setEnvVars.js │ ├── cssTransform.js │ └── fileTransform.js ├── pnpTs.js ├── getHttpsConfig.js ├── paths.js ├── modules.js └── env.js ├── .travis.yml ├── .gitignore ├── Dockerfile ├── .github ├── workflows │ ├── cd.yml │ └── ci.yml └── ISSUE_TEMPLATE.md ├── example.config.js ├── index.html ├── scripts ├── test.js └── start.js ├── test └── server_process.js ├── README.md ├── CONTRIBUTING.md ├── LICENSE.md └── package.json /.env: -------------------------------------------------------------------------------- 1 | FAST_REFRESH=true -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | example.config.js 4 | -------------------------------------------------------------------------------- /src/scss/mediaplayer.scss: -------------------------------------------------------------------------------- 1 | @import "~video-react/styles/scss/video-react"; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaearon/ukraine-timemap/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/fa-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaearon/ukraine-timemap/HEAD/src/assets/fa-logo.png -------------------------------------------------------------------------------- /docs/example-timemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaearon/ukraine-timemap/HEAD/docs/example-timemap.png -------------------------------------------------------------------------------- /src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaearon/ukraine-timemap/HEAD/src/components/.DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore Create React App-related scaffolding 2 | config/ 3 | scripts/ 4 | test/server_process.js -------------------------------------------------------------------------------- /src/assets/bellingcat-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaearon/ukraine-timemap/HEAD/src/assets/bellingcat-logo.png -------------------------------------------------------------------------------- /src/assets/placeholder-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaearon/ukraine-timemap/HEAD/src/assets/placeholder-image.jpg -------------------------------------------------------------------------------- /src/assets/fonts/timemapfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaearon/ukraine-timemap/HEAD/src/assets/fonts/timemapfont.woff -------------------------------------------------------------------------------- /src/assets/satelliteoverlaytoggle/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaearon/ukraine-timemap/HEAD/src/assets/satelliteoverlaytoggle/map.png -------------------------------------------------------------------------------- /src/assets/satelliteoverlaytoggle/sat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaearon/ukraine-timemap/HEAD/src/assets/satelliteoverlaytoggle/sat.png -------------------------------------------------------------------------------- /config/jest/setEnvVars.js: -------------------------------------------------------------------------------- 1 | const envConfig = require("../../" + (process.env.CONFIG || 'config.js')); 2 | process.env = { ...process.env, ...envConfig }; 3 | -------------------------------------------------------------------------------- /src/reducers/features.js: -------------------------------------------------------------------------------- 1 | import initial from "../store/initial.js"; 2 | 3 | function features(featureState = initial.features, action) { 4 | return featureState; 5 | } 6 | 7 | export default features; 8 | -------------------------------------------------------------------------------- /src/reducers/validate/regionSchema.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | const regionSchema = Joi.object().keys({ 4 | name: Joi.string().required(), 5 | items: Joi.array().required(), 6 | }); 7 | 8 | export default regionSchema; 9 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import "../scss/main.scss"; 2 | import React from "react"; 3 | import Layout from "./Layout"; 4 | 5 | class App extends React.Component { 6 | render() { 7 | return ; 8 | } 9 | } 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /src/components/atoms/StaticPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const StaticPage = ({ showing, children }) => ( 4 |
5 | {children} 6 |
7 | ); 8 | 9 | export default StaticPage; 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | cache: 5 | directories: 6 | - node_modules 7 | before_script: 8 | - cp example.config.js config.js 9 | install: 10 | - npm install 11 | script: 12 | - npm run lint 13 | - npm run build 14 | - npm run test 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | build/ 3 | node_modules/ 4 | 5 | dev.config.js 6 | !config/webpack*.config.js 7 | !config/getHttpsConfig.js 8 | 9 | 10 | tags 11 | tags.lock 12 | tags.temp 13 | 14 | .eslintcache 15 | 16 | src/\.DS_Store 17 | src/assets/fonts 18 | 19 | \.DS_Store 20 | 21 | tags 22 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // Add globals here 2 | // --- 3 | // In the example below we're providing no-ops for the logging functions 4 | 5 | // global.console = { 6 | // log: jest.fn(), 7 | // error: jest.fn(), 8 | // warn: jest.fn(), 9 | // info: jest.fn(), 10 | // debug: jest.fn(), 11 | // }; 12 | -------------------------------------------------------------------------------- /src/reducers/validate/shapeSchema.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | const shapeSchema = Joi.object().keys({ 4 | id: Joi.string().allow(""), 5 | title: Joi.string().allow(""), 6 | shape: Joi.string().allow(""), 7 | colour: Joi.string().allow(""), 8 | }); 9 | 10 | export default shapeSchema; 11 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import domain from "./domain.js"; 4 | import app from "./app.js"; 5 | import ui from "./ui.js"; 6 | import features from "./features.js"; 7 | 8 | export default combineReducers({ 9 | app, 10 | domain, 11 | ui, 12 | features, 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/atoms/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Spinner = ({ small }) => { 4 | return ( 5 |
6 |
7 |
8 |
9 | ); 10 | }; 11 | 12 | export default Spinner; 13 | -------------------------------------------------------------------------------- /src/scss/_icons.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | display: inline-block; 3 | width: 32px; 4 | height: 1em; 5 | stroke-width: 0; 6 | stroke: $offwhite; 7 | fill: $offwhite; 8 | transform: scale(1.4); 9 | cursor: pointer; 10 | 11 | &:hover { 12 | transition: 0.2s ease; 13 | stroke: $yellow; 14 | fill: $yellow; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:10.11 2 | 3 | LABEL authors="Lachlan Kermode " 4 | 5 | # Install app dependencies 6 | COPY package.json /www/package.json 7 | RUN cd /www; yarn 8 | 9 | # Copy app source 10 | COPY . /www 11 | WORKDIR /www 12 | RUN yarn build 13 | 14 | # files available to copy at /www/build 15 | -------------------------------------------------------------------------------- /src/common/global.js: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | fa_red: "#eb443e", 3 | yellow: "#ffd800", 4 | black: "#000", 5 | white: "#fff", 6 | }; 7 | 8 | const exports = { 9 | fallbackEventColor: colors.fa_red, 10 | darkBackground: colors.black, 11 | primaryHighlight: colors.fa_red, 12 | secondaryHighlight: colors.white, 13 | }; 14 | 15 | export default exports; 16 | -------------------------------------------------------------------------------- /src/components/space/Space.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MapCarto from "./carto/Map"; 3 | // import Map3d from "./3d/Map"; 4 | 5 | const Space = (props) => { 6 | switch (props.kind) { 7 | // case "3d": 8 | // return ; 9 | default: 10 | return ; 11 | } 12 | }; 13 | 14 | export default Space; 15 | -------------------------------------------------------------------------------- /src/components/time/atoms/Clip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const TimelineClip = ({ dims }) => ( 4 | 5 | 11 | 12 | ); 13 | 14 | export default TimelineClip; 15 | -------------------------------------------------------------------------------- /src/reducers/validate/associationsSchema.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | const associationsSchema = Joi.object().keys({ 4 | id: Joi.string().allow("").required(), 5 | title: Joi.string().allow("").required(), 6 | desc: Joi.string().allow(""), 7 | mode: Joi.string().allow("").required(), 8 | filter_paths: Joi.array(), 9 | }); 10 | 11 | export default associationsSchema; 12 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from "redux"; 2 | import thunk from "redux-thunk"; 3 | 4 | import rootReducer from "../reducers"; 5 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 6 | 7 | const store = createStore( 8 | rootReducer, 9 | composeEnhancers(applyMiddleware(thunk)) 10 | ); 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return "module.exports = {};"; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return "cssTransform"; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/controls/atoms/Caret.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CardCaret = ({ isOpen, toggle }) => { 4 | let classes = isOpen ? "arrow-down" : "arrow-down folded"; 5 | 6 | return ( 7 |
8 |

9 | 10 |

11 |
12 | ); 13 | }; 14 | 15 | export default CardCaret; 16 | -------------------------------------------------------------------------------- /src/reducers/validate/siteSchema.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | const siteSchema = Joi.object().keys({ 4 | id: Joi.string().required(), 5 | description: Joi.string().allow("").required(), 6 | site: Joi.string().required(), 7 | latitude: Joi.string().required(), 8 | longitude: Joi.string().required(), 9 | enabled: Joi.string().allow(""), 10 | }); 11 | 12 | export default siteSchema; 13 | -------------------------------------------------------------------------------- /src/components/controls/atoms/ToolbarButton.js: -------------------------------------------------------------------------------- 1 | export function ToolbarButton({ isActive, iconKey, onClick, label }) { 2 | return ( 3 |
8 | {iconKey} 9 |
{label}
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "common"; 3 | @import "loading"; 4 | @import "header"; 5 | @import "cardstack"; 6 | @import "narrativecard"; 7 | @import "overlay"; 8 | @import "map"; 9 | @import "timeline"; 10 | @import "toolbar"; 11 | @import "infopopup"; 12 | @import "notification"; 13 | @import "mediaplayer"; 14 | @import "cover"; 15 | @import "search"; 16 | @import "satelliteoverlaytoggle"; 17 | -------------------------------------------------------------------------------- /src/components/controls/atoms/CustomField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import marked from "marked"; 3 | 4 | // TODO could this be a security vulnerability? 5 | const CardCustomField = ({ title, value }) => ( 6 |
7 | {title ?

{title}

: null} 8 |
9 |
10 | ); 11 | 12 | export default CardCustomField; 13 | -------------------------------------------------------------------------------- /src/components/controls/atoms/NarrativeClose.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Close = ({ onClickHandler, closeMsg }) => { 4 | return ( 5 |
6 | 9 |
{closeMsg}
10 |
11 | ); 12 | }; 13 | 14 | export default Close; 15 | -------------------------------------------------------------------------------- /src/components/time/atoms/DatetimeDot.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default ({ 4 | category, 5 | events, 6 | x, 7 | y, 8 | r, 9 | onSelect, 10 | styleProps, 11 | extraRender, 12 | }) => { 13 | if (!y) return null; 14 | return ( 15 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/InfoPopup.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Popup from "./atoms/Popup"; 3 | import copy from "../common/data/copy.json"; 4 | 5 | const Infopopup = ({ isOpen, onClose, language, styles }) => ( 6 | 13 | ); 14 | 15 | export default Infopopup; 16 | -------------------------------------------------------------------------------- /src/components/atoms/CoverIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CoverIcon = ({ isActive, isDisabled, onClickHandler }) => { 4 | let classes = isActive ? "action-button enabled" : "action-button"; 5 | if (isDisabled) { 6 | classes = "action-button disabled"; 7 | } 8 | 9 | return ( 10 | 13 | ); 14 | }; 15 | 16 | export default CoverIcon; 17 | -------------------------------------------------------------------------------- /src/components/atoms/InfoIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CoverIcon = ({ isActive, isDisabled, onClickHandler }) => { 4 | let classes = isActive ? "action-button enabled" : "action-button"; 5 | if (isDisabled) { 6 | classes = "action-button disabled"; 7 | } 8 | 9 | return ( 10 | 13 | ); 14 | }; 15 | 16 | export default CoverIcon; 17 | -------------------------------------------------------------------------------- /src/components/controls/atoms/NarrativeAdjust.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Adjust = ({ isDisabled, direction, onClickHandler }) => { 4 | return ( 5 |
9 | 10 | {`chevron_${direction}`} 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Adjust; 17 | -------------------------------------------------------------------------------- /src/components/atoms/SitesIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SitesIcon = ({ isActive, isDisabled, onClickHandler }) => { 4 | let classes = isActive ? "action-button enabled" : "action-button"; 5 | if (isDisabled) { 6 | classes = "action-button disabled"; 7 | } 8 | 9 | return ( 10 | 13 | ); 14 | }; 15 | 16 | export default SitesIcon; 17 | -------------------------------------------------------------------------------- /src/test/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import "@testing-library/jest-dom"; 4 | import { Provider } from "react-redux"; 5 | 6 | import store from "../store/"; 7 | import App from "../components/App"; 8 | 9 | it("renders an option to view categories", () => { 10 | render( 11 | 12 | 13 | 14 | ); 15 | expect(screen.getByText("Filters")).toBeInTheDocument(); 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/time/atoms/DatetimeSquare.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DatetimeSquare = ({ 4 | x, 5 | y, 6 | r, 7 | transform, 8 | onSelect, 9 | styleProps, 10 | extraRender, 11 | }) => { 12 | return ( 13 | 23 | ); 24 | }; 25 | 26 | export default DatetimeSquare; 27 | -------------------------------------------------------------------------------- /src/components/time/atoms/DatetimeTriangle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DatetimeTriangle = ({ x, y, r, transform, onSelect, styleProps }) => { 4 | const s = (r * 2) / 3; 5 | return ( 6 | 15 | ); 16 | }; 17 | 18 | export default DatetimeTriangle; 19 | -------------------------------------------------------------------------------- /src/components/atoms/NoSource.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NoSource = ({ failedUrls }) => { 4 | return ( 5 |
6 |
7 |

8 | error 9 |

10 |

11 | No media found, as the original media has not yet been uploaded to the 12 | platform. 13 |

14 |
15 |
16 | ); 17 | }; 18 | 19 | export default NoSource; 20 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | on: 3 | push: 4 | branches: [ develop ] 5 | # pull_request: 6 | # branches: [ develop ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Trigger CD build 13 | uses: peter-evans/repository-dispatch@v1 14 | with: 15 | token: ${{ secrets.CI_DISPATCH_TOKEN }} 16 | repository: forensic-architecture/configs 17 | event-type: remote-build 18 | client-payload: '{"runtime_args": "timemap", "branch": "${GITHUB_REF##*/}"}' 19 | 20 | -------------------------------------------------------------------------------- /src/components/time/atoms/DatetimePentagon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DatetimePentagon = ({ x, y, r, transform, onSelect, styleProps }) => { 4 | const s = (r * 2) / 3; 5 | return ( 6 | 17 | ); 18 | }; 19 | 20 | export default DatetimePentagon; 21 | -------------------------------------------------------------------------------- /src/reducers/validate/sourceSchema.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | const sourceSchema = Joi.object().keys({ 4 | id: Joi.string().required(), 5 | title: Joi.string().allow(""), 6 | thumbnail: Joi.string().allow(""), 7 | paths: Joi.array().required(), 8 | type: Joi.string().allow(""), 9 | affil_s: Joi.array().allow(""), 10 | url: Joi.string().allow(""), 11 | description: Joi.string().allow(""), 12 | parent: Joi.string().allow(""), 13 | author: Joi.string().allow(""), 14 | date: Joi.string().allow(""), 15 | notes: Joi.string().allow(""), 16 | }); 17 | 18 | export default sourceSchema; 19 | -------------------------------------------------------------------------------- /src/components/time/atoms/DatetimeStar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DatetimeStar = ({ 4 | x, 5 | y, 6 | r, 7 | transform, 8 | onSelect, 9 | styleProps, 10 | extraRender, 11 | }) => { 12 | const s = (r * 2) / 3; 13 | return ( 14 | 25 | ); 26 | }; 27 | 28 | export default DatetimeStar; 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ develop ] 5 | pull_request: 6 | branches: [ develop ] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | ref: ${{ github.head_ref }} 15 | - uses: actions/setup-node@v2-beta 16 | with: 17 | node-version: '12' 18 | 19 | - run: npm install 20 | - run: cp example.config.js config.js 21 | - run: npm test 22 | env: 23 | CI: true 24 | - run: npm run lint 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /src/components/atoms/RouteIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const RouteIcon = ({ isEnabled, toggleMapViews }) => { 4 | return ( 5 | 18 | ); 19 | }; 20 | 21 | export default RouteIcon; 22 | -------------------------------------------------------------------------------- /src/components/time/atoms/Project.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Project = ({ 4 | offset, 5 | id, 6 | start, 7 | end, 8 | getX, 9 | y, 10 | dims, 11 | colour, 12 | eventRadius, 13 | onClick, 14 | }) => { 15 | const length = getX(end) - getX(start); 16 | if (offset === undefined) return null; 17 | return ( 18 | 27 | ); 28 | }; 29 | 30 | export default Project; 31 | -------------------------------------------------------------------------------- /src/assets/checkbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/scss/video.scss: -------------------------------------------------------------------------------- 1 | .video-wrapper { 2 | z-index: 1; 3 | position: relative; 4 | width: 740px; 5 | height: 420px; 6 | transition: opacity 500ms; 7 | background-color: black; 8 | overflow: hidden; 9 | } 10 | 11 | .video-js .vjs-big-play-button { 12 | font-size: 3em; 13 | line-height: 40px; 14 | height: 40px; 15 | width: 40px; 16 | display: block; 17 | position: absolute; 18 | background: none; 19 | top: 10px; 20 | left: 10px; 21 | padding: 0; 22 | cursor: pointer; 23 | opacity: 1; 24 | border-radius: 20px; 25 | transition: 0.2s ease; 26 | border: 1px solid $midwhite; 27 | 28 | &:hover { 29 | transition: 0.2s ease; 30 | border: 1px solid $offwhite; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/reducers/ui.js: -------------------------------------------------------------------------------- 1 | import initial from "../store/initial.js"; 2 | 3 | import { USE_SATELLITE_TILES_OVERLAY, RESET_TILES_OVERLAY } from "../actions"; 4 | 5 | function ui(uiState = initial.ui, action) { 6 | switch (action.type) { 7 | case USE_SATELLITE_TILES_OVERLAY: 8 | return { 9 | ...uiState, 10 | tiles: { 11 | ...uiState.tiles, 12 | current: "satellite", 13 | }, 14 | }; 15 | case RESET_TILES_OVERLAY: 16 | return { 17 | ...uiState, 18 | tiles: { 19 | ...uiState.tiles, 20 | current: uiState.tiles.default, 21 | }, 22 | }; 23 | default: 24 | return uiState; 25 | } 26 | } 27 | 28 | export default ui; 29 | -------------------------------------------------------------------------------- /src/components/atoms/Loading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import copy from "../../common/data/copy.json"; 3 | 4 | const LoadingOverlay = ({ isLoading, language }) => { 5 | let classes = "loading-overlay"; 6 | classes += !isLoading ? " hidden" : ""; 7 | 8 | return ( 9 |
10 |
11 | 12 | {copy[language].loading} 13 | 14 |
15 |
16 |
17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default LoadingOverlay; 24 | -------------------------------------------------------------------------------- /src/components/space/carto/atoms/Sites.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function MapSites({ sites, projectPoint }) { 4 | function renderSite(site) { 5 | const { x, y } = projectPoint([site.latitude, site.longitude]); 6 | 7 | return ( 8 |
15 | {site.site} 16 |
17 | ); 18 | } 19 | 20 | if (!sites || !sites.length) return null; 21 | 22 | return
{sites.map(renderSite)}
; 23 | } 24 | 25 | export default MapSites; 26 | -------------------------------------------------------------------------------- /src/components/time/atoms/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeNiceDate } from "../../../common/utilities"; 3 | 4 | const TimelineHeader = ({ title, from, to, onClick, hideInfo }) => { 5 | const d0 = from && makeNiceDate(from); 6 | const d1 = to && makeNiceDate(to); 7 | return ( 8 |
9 |
onClick()}> 10 |

11 | 12 |

13 |
14 |
15 |

{title}

16 |

17 | {d0} - {d1} 18 |

19 |
20 |
21 | ); 22 | }; 23 | 24 | export default TimelineHeader; 25 | -------------------------------------------------------------------------------- /src/components/atoms/RefreshIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default ({ isActive, isDisabled, onClickHandler }) => { 4 | return ( 5 | 14 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/selectors/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Some handy helpers 3 | */ 4 | 5 | /** 6 | * Given an event and a time range, 7 | * returns true/false if the event falls within timeRange 8 | */ 9 | export function isTimeRangedIn(event, timeRange) { 10 | const eventTime = event.datetime; 11 | return timeRange[0] < eventTime && eventTime < timeRange[1]; 12 | } 13 | 14 | /** 15 | * Shuffles array in place. ES6 version 16 | * @param {Array} a items An array containing the items. 17 | * https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array 18 | */ 19 | export function shuffle(a) { 20 | for (let i = a.length - 1; i > 0; i--) { 21 | const j = Math.floor(Math.random() * (i + 1)); 22 | [a[i], a[j]] = [a[j], a[i]]; 23 | } 24 | return a; 25 | } 26 | -------------------------------------------------------------------------------- /config/pnpTs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { resolveModuleName } = require('ts-pnp'); 4 | 5 | exports.resolveModuleName = ( 6 | typescript, 7 | moduleName, 8 | containingFile, 9 | compilerOptions, 10 | resolutionHost 11 | ) => { 12 | return resolveModuleName( 13 | moduleName, 14 | containingFile, 15 | compilerOptions, 16 | resolutionHost, 17 | typescript.resolveModuleName 18 | ); 19 | }; 20 | 21 | exports.resolveTypeReferenceDirective = ( 22 | typescript, 23 | moduleName, 24 | containingFile, 25 | compilerOptions, 26 | resolutionHost 27 | ) => { 28 | return resolveModuleName( 29 | moduleName, 30 | containingFile, 31 | compilerOptions, 32 | resolutionHost, 33 | typescript.resolveTypeReferenceDirective 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /example.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'example', 3 | display_title: 'example', 4 | SERVER_ROOT: 'http://localhost:4040', 5 | EVENTS_EXT: '/api/timemap_data/export_events/deeprows', 6 | ASSOCIATIONS_EXT: '/api/timemap_data/export_associations/deeprows', 7 | SOURCES_EXT: '/api/timemap_data/export_sources/deepids', 8 | SITES_EXT: '', 9 | SHAPES_EXT: '', 10 | DATE_FMT: 'MM/DD/YYYY', 11 | TIME_FMT: 'hh:mm', 12 | store: { 13 | app: { 14 | map: { 15 | anchor: [31.356397, 34.784818] 16 | } 17 | }, 18 | features: { 19 | COLOR_BY_ASSOCIATION: true, 20 | USE_ASSOCIATIONS: true, 21 | USE_SOURCES: true, 22 | USE_COVER: false, 23 | GRAPH_NONLOCATED: false, 24 | HIGHLIGHT_GROUPS: false 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/common/data/es-MX.json: -------------------------------------------------------------------------------- 1 | { 2 | "dateTime": "%x, %X", 3 | "date": "%d/%m/%Y", 4 | "time": "%-I:%M:%S %p", 5 | "periods": ["AM", "PM"], 6 | "days": [ 7 | "domingo", 8 | "lunes", 9 | "martes", 10 | "miércoles", 11 | "jueves", 12 | "viernes", 13 | "sábado" 14 | ], 15 | "shortDays": ["dom", "lun", "mar", "mié", "jue", "vie", "sáb"], 16 | "months": [ 17 | "enero", 18 | "febrero", 19 | "marzo", 20 | "abril", 21 | "mayo", 22 | "junio", 23 | "julio", 24 | "agosto", 25 | "septiembre", 26 | "octubre", 27 | "noviembre", 28 | "diciembre" 29 | ], 30 | "shortMonths": [ 31 | "ene", 32 | "feb", 33 | "mar", 34 | "abr", 35 | "may", 36 | "jun", 37 | "jul", 38 | "ago", 39 | "sep", 40 | "oct", 41 | "nov", 42 | "dic" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/scss/button.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | font-weight: normal; 4 | border: 0; 5 | border-radius: 0em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | outline: none; 10 | text-align: left; 11 | } 12 | .button--primary { 13 | color: $offwhite; 14 | background-color: $default; 15 | } 16 | .button--secondary { 17 | color: #333; 18 | background-color: transparent; 19 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 2px inset; 20 | } 21 | .button--small { 22 | font-size: 12px; 23 | padding: 10px 16px; 24 | margin: 0.6em 0.3em 0 0; 25 | } 26 | .button--medium { 27 | font-size: 14px; 28 | padding: 11px 20px; 29 | } 30 | .button--large { 31 | font-size: 16px; 32 | padding: 12px 24px; 33 | } 34 | 35 | .no-hover { 36 | cursor: auto !important; 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/arrowdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/controls/CategoriesListPanel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import marked from "marked"; 3 | import PanelTree from "./atoms/PanelTree"; 4 | import { ASSOCIATION_MODES } from "../../common/constants"; 5 | 6 | const CategoriesListPanel = ({ 7 | categories, 8 | activeCategories, 9 | onCategoryFilter, 10 | language, 11 | title, 12 | description, 13 | }) => { 14 | return ( 15 |
16 |

{title}

17 |

22 | 28 |

29 | ); 30 | }; 31 | 32 | export default CategoriesListPanel; 33 | -------------------------------------------------------------------------------- /src/reducers/__tests__/ui.spec.js: -------------------------------------------------------------------------------- 1 | import { useSatelliteTilesOverlay, resetTilesOverlay } from "../../actions"; 2 | import initial from "../../store/initial.js"; 3 | import ui from "../ui"; 4 | 5 | describe("UI reducer", () => { 6 | it("can change the tiling", () => { 7 | const result = ui(initial.ui, useSatelliteTilesOverlay()); 8 | expect(result.tiles.current).toEqual("satellite"); 9 | expect(result.tiles.default).toEqual(initial.ui.tiles.default); 10 | }); 11 | 12 | it("can revert to the default tiling", () => { 13 | const result = ui( 14 | { 15 | ...initial.ui, 16 | tiles: { default: "some default", current: "something else" }, 17 | }, 18 | resetTilesOverlay() 19 | ); 20 | expect(result.tiles.current).toEqual("some default"); 21 | expect(result.tiles.default).toEqual("some default"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/space/carto/atoms/DefsMarkers.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const MapDefsMarkers = () => ( 4 | 5 | 6 | 15 | 16 | 17 | 26 | 30 | 31 | 32 | 33 | ); 34 | 35 | export default MapDefsMarkers; 36 | -------------------------------------------------------------------------------- /src/scss/satelliteoverlaytoggle.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .satellite-overlay-toggle { 4 | background-color: rgb(53, 53, 53); 5 | opacity: 0.9; 6 | position: fixed; 7 | top: 0.5em; 8 | right: 0.5em; 9 | z-index: $map-overlay; 10 | cursor: pointer; 11 | 12 | .satellite-overlay-toggle-button { 13 | width: 64px; 14 | height: 64px; 15 | opacity: 0.8; 16 | box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); 17 | border: none; 18 | color: white; 19 | user-select: none; 20 | display: flex; 21 | justify-content: center; 22 | align-items: flex-end; 23 | padding-bottom: 0.5em; 24 | text-transform: uppercase; 25 | 26 | &.satellite-overlay-toggle-map { 27 | color: black; 28 | } 29 | 30 | &:hover { 31 | opacity: 1; 32 | } 33 | 34 | .label { 35 | font-size: $normal; 36 | font-family: $mainfont; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/time/atoms/Labels.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const TimelineLabels = ({ dims, timelabels }) => { 4 | return ( 5 | 6 | 13 | 20 | 21 | {timelabels[0]} 22 | 23 | 29 | {timelabels[1]} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default TimelineLabels; 36 | -------------------------------------------------------------------------------- /src/components/controls/ShapesListPanel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import marked from "marked"; 3 | import PanelTree from "./atoms/PanelTree"; 4 | import { mapStyleByShape } from "../../common/utilities"; 5 | import { SHAPE } from "../../common/constants"; 6 | 7 | const ShapesListPanel = ({ 8 | shapes, 9 | activeShapes, 10 | onShapeFilter, 11 | language, 12 | title, 13 | description, 14 | }) => { 15 | const styledShapes = mapStyleByShape(shapes, activeShapes); 16 | return ( 17 |
18 |

{title}

19 |

24 | 30 |

31 | ); 32 | }; 33 | 34 | export default ShapesListPanel; 35 | -------------------------------------------------------------------------------- /src/components/controls/atoms/Time.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import copy from "../../../common/data/copy.json"; 4 | import { isNotNullNorUndefined } from "../../../common/utilities"; 5 | 6 | const CardTime = ({ title = "Timestamp", timelabel, language, precision }) => { 7 | const unknownLang = copy[language].cardstack.unknown_time; 8 | 9 | if (isNotNullNorUndefined(timelabel)) { 10 | return ( 11 |
12 | {/* today */} 13 |

{title}

14 | {timelabel} 15 | {precision && precision !== "" ? ` - ${precision}` : null} 16 |
17 | ); 18 | } else { 19 | return ( 20 |
21 | {/* today */} 22 |

{title}

23 | {unknownLang} 24 |
25 | ); 26 | } 27 | }; 28 | 29 | export default CardTime; 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TimeMap - Forensic Architecture 6 | 7 | 8 | 9 | 10 | 11 | 12 | 21 |
22 |
23 |
24 |
25 |
26 | This platform is unsuitable for mobile. Please revisit on a desktop. 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /src/common/constants.js: -------------------------------------------------------------------------------- 1 | export const ASSOCIATION_MODES = { 2 | CATEGORY: "CATEGORY", 3 | NARRATIVE: "NARRATIVE", 4 | FILTER: "FILTER", 5 | }; 6 | 7 | export const SHAPE = "SHAPE"; 8 | 9 | export const DEFAULT_TAB_ICONS = { 10 | CATEGORY: "widgets", 11 | NARRATIVE: "timeline", 12 | FILTER: "filter_list", 13 | SHAPE: "change_history", 14 | }; 15 | 16 | export const AVAILABLE_SHAPES = { 17 | STAR: "STAR", 18 | DIAMOND: "DIAMOND", 19 | PENTAGON: "PENTAGON", 20 | SQUARE: "SQUARE", 21 | DOT: "DOT", 22 | BAR: "BAR", 23 | TRIANGLE: "TRIANGLE", 24 | }; 25 | 26 | export const POLYGON_CLIP_PATH = { 27 | STAR: 28 | "polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)", 29 | DIAMOND: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)", 30 | PENTAGON: "polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)", 31 | TRIANGLE: "polygon(50% 0%, 0% 100%, 100% 100%)", 32 | }; 33 | 34 | export const DEFAULT_CHECKBOX_COLOR = "#ffffff"; 35 | -------------------------------------------------------------------------------- /src/components/atoms/Controls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const OverlayControls = ({ viewIdx, paths, onShiftHandler }) => { 4 | const backArrow = 5 | viewIdx !== 0 ? ( 6 |
onShiftHandler(-1)}> 7 |
8 | arrow_left 9 |
10 |
11 | ) : null; 12 | const forwardArrow = 13 | viewIdx < paths.length - 1 ? ( 14 |
onShiftHandler(1)}> 15 |
16 | arrow_right 17 |
18 |
19 | ) : null; 20 | 21 | if (paths.length > 1) { 22 | return ( 23 |
24 | {backArrow} 25 | {forwardArrow} 26 |
27 | ); 28 | } 29 | return
; 30 | }; 31 | 32 | export default OverlayControls; 33 | -------------------------------------------------------------------------------- /src/reducers/domain.js: -------------------------------------------------------------------------------- 1 | import initial from "../store/initial.js"; 2 | 3 | import { UPDATE_DOMAIN, MARK_NOTIFICATIONS_READ } from "../actions"; 4 | import { validateDomain } from "./validate/validators.js"; 5 | 6 | function updateDomain(domainState, action) { 7 | return { 8 | ...domainState, 9 | ...validateDomain(action.payload.domain, action.payload.features), 10 | }; 11 | } 12 | 13 | function markNotificationsRead(domainState, action) { 14 | return { 15 | ...domainState, 16 | notifications: domainState.notifications.map((n) => ({ 17 | ...n, 18 | isRead: true, 19 | })), 20 | }; 21 | } 22 | 23 | function domain(domainState = initial.domain, action) { 24 | switch (action.type) { 25 | case UPDATE_DOMAIN: 26 | return updateDomain(domainState, action); 27 | case MARK_NOTIFICATIONS_READ: 28 | return markNotificationsRead(domainState, action); 29 | default: 30 | return domainState; 31 | } 32 | } 33 | 34 | export default domain; 35 | -------------------------------------------------------------------------------- /src/components/atoms/Popup.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import marked from "marked"; 3 | 4 | const fontSize = window.innerWidth > 1000 ? 14 : 18; 5 | 6 | const Popup = ({ 7 | content = [], 8 | styles = {}, 9 | isOpen = true, 10 | onClose, 11 | title, 12 | theme = "light", 13 | children, 14 | }) => ( 15 |
16 |
22 |
23 | 29 |

{title}

30 |
31 | {content.map((t, idx) => ( 32 |
33 | ))} 34 | {children} 35 |
36 |
37 | ); 38 | 39 | export default Popup; 40 | -------------------------------------------------------------------------------- /src/scss/_video.scss: -------------------------------------------------------------------------------- 1 | .fullscreen-bg { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | overflow: hidden; 8 | z-index: -100; 9 | } 10 | 11 | .fullscreen-bg__video { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | width: 100%; 16 | height: 100%; 17 | -webkit-filter: contrast(70%) brightness(70%) grayscale(30%); /* Webkit */ 18 | filter: gray; /* IE6-9 */ 19 | filter: contrast(70%) brightness(70%) grayscale(30%); /* W3C */ 20 | } 21 | 22 | @media (min-aspect-ratio: 16/9) { 23 | .fullscreen-bg__video { 24 | height: 300%; 25 | top: -100%; 26 | } 27 | } 28 | 29 | @media (max-aspect-ratio: 16/9) { 30 | .fullscreen-bg__video { 31 | width: 300%; 32 | left: -100%; 33 | } 34 | } 35 | 36 | @media (max-width: 767px) { 37 | .fullscreen-bg { 38 | background: url("/static/archive/img/city.jpg") center center / cover 39 | no-repeat; 40 | } 41 | 42 | .fullscreen-bg__video { 43 | display: none; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/controls/NarrativeControls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Card from "./atoms/NarrativeCard"; 3 | import Adjust from "./atoms/NarrativeAdjust"; 4 | import Close from "./atoms/NarrativeClose"; 5 | 6 | const NarrativeControls = ({ narrative, methods }) => { 7 | if (!narrative) return null; 8 | 9 | const { current, steps } = narrative; 10 | const prevExists = current !== 0; 11 | const nextExists = current < steps.length - 1; 12 | 13 | return ( 14 | <> 15 | 16 | 21 | 26 | methods.onSelectNarrative(null)} 28 | closeMsg="-- exit from narrative --" 29 | /> 30 | 31 | ); 32 | }; 33 | 34 | export default NarrativeControls; 35 | -------------------------------------------------------------------------------- /src/components/atoms/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DEFAULT_CHECKBOX_COLOR } from "../../common/constants"; 3 | 4 | const Checkbox = ({ label, isActive, onClickCheckbox, color, styleProps }) => { 5 | const checkboxColor = color ? color : DEFAULT_CHECKBOX_COLOR; 6 | const baseStyles = { 7 | checkboxStyles: { 8 | background: isActive ? checkboxColor : "none", 9 | border: `1px solid ${checkboxColor}`, 10 | }, 11 | }; 12 | const containerStyles = styleProps ? styleProps.containerStyles : {}; 13 | const checkboxStyles = styleProps 14 | ? styleProps.checkboxStyles 15 | : baseStyles.checkboxStyles; 16 | return ( 17 |
18 | {label} 19 | 24 |
25 | ); 26 | }; 27 | 28 | export default Checkbox; 29 | -------------------------------------------------------------------------------- /src/scss/tabs.scss: -------------------------------------------------------------------------------- 1 | .react-tabs { 2 | padding-top: 0; 3 | box-sizing: border-box; 4 | height: 100%; 5 | 6 | [role="tablist"] { 7 | padding: 0; 8 | } 9 | 10 | [role="tab"] { 11 | font-size: $xlarge; 12 | width: 33%; 13 | background: none; 14 | color: $midwhite; 15 | outline: none; 16 | float: left; 17 | cursor: pointer; 18 | text-align: center; 19 | height: 40px; 20 | line-height: 40px; 21 | border-bottom: 1px solid rgba(255, 255, 255, 0.4); 22 | list-style-type: none; 23 | box-sizing: border-box; 24 | &:hover { 25 | color: $offwhite; 26 | } 27 | } 28 | 29 | [role="tab"][aria-selected="true"] { 30 | font-weight: 700; 31 | border-radius: 0; 32 | border: 0; 33 | color: $offwhite; 34 | border: 1px solid; 35 | box-sizing: border-box; 36 | text-align: center; 37 | border: 1px solid rgba(255, 255, 255, 0.4); 38 | border-bottom: 0; 39 | } 40 | 41 | .react-innertabpanel { 42 | box-sizing: border-box; 43 | padding-top: 20px; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/time/atoms/DatetimeBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DatetimeBar = ({ 4 | highlights, 5 | events, 6 | x, 7 | y, 8 | width, 9 | height, 10 | onSelect, 11 | styleProps, 12 | extraRender, 13 | }) => { 14 | if (highlights.length === 0) { 15 | return ( 16 | 25 | ); 26 | } 27 | const sectionHeight = height / highlights.length; 28 | return ( 29 | <> 30 | {highlights.map((h, idx) => ( 31 | 40 | ))} 41 | 42 | ); 43 | }; 44 | 45 | export default DatetimeBar; 46 | -------------------------------------------------------------------------------- /src/components/controls/atoms/PanelTree.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Checkbox from "../../atoms/Checkbox"; 3 | import { ASSOCIATION_MODES } from "../../../common/constants"; 4 | 5 | const PanelTree = ({ data, activeValues, onSelect, type }) => { 6 | // If the parent panel is of type 'CATEGORY': filter on title. If panel is 'SHAPE': filter on id 7 | const onSelectionType = type === ASSOCIATION_MODES.CATEGORY ? "title" : "id"; 8 | return ( 9 |
10 | {data.map((val) => { 11 | return ( 12 |
  • 17 | onSelect(val[onSelectionType])} 21 | styleProps={val.styles} 22 | /> 23 |
  • 24 | ); 25 | })} 26 |
    27 | ); 28 | }; 29 | 30 | export default PanelTree; 31 | -------------------------------------------------------------------------------- /src/components/time/atoms/Handles.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const TimelineHandles = ({ dims, onMoveTime }) => { 4 | const transform = "scale(1.5,1.5)"; 5 | const size = 45; 6 | const handleOffset = dims.contentHeight / 2; 7 | return ( 8 | 9 | onMoveTime("backwards")} 12 | > 13 | 14 | 18 | 19 | onMoveTime("forward")} 24 | > 25 | 26 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default TimelineHandles; 36 | -------------------------------------------------------------------------------- /src/components/controls/atoms/NarrativeCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { selectActiveNarrative } from "../../../selectors"; 4 | 5 | function NarrativeCard({ narrative }) { 6 | // no display if no narrative 7 | const { steps, current } = narrative; 8 | 9 | if (steps[current]) { 10 | return ( 11 |
    12 |
    13 |
    14 |
    15 | {current + 1}/{steps.length} 16 |
    17 |
    18 |
    19 |

    {narrative.label}

    20 |
    21 |
    22 | 23 | {/* location_on */} 24 | {/* {_renderActions(current, steps)} */} 25 |
    26 |

    {narrative.description}

    27 |
    28 |
    29 | ); 30 | } else { 31 | return null; 32 | } 33 | } 34 | 35 | function mapStateToProps(state) { 36 | return { 37 | narrative: selectActiveNarrative(state), 38 | }; 39 | } 40 | export default connect(mapStateToProps)(NarrativeCard); 41 | -------------------------------------------------------------------------------- /src/components/atoms/Md.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import marked from "marked"; 4 | 5 | class Md extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { md: null, error: null }; 9 | } 10 | 11 | componentDidMount() { 12 | fetch(this.props.path) 13 | .then((resp) => resp.text()) 14 | .then((text) => { 15 | if (text.length <= 0) { 16 | throw new Error(); 17 | } 18 | 19 | this.setState({ md: marked(text) }); 20 | }) 21 | .catch(() => { 22 | this.setState({ error: true }); 23 | }); 24 | } 25 | 26 | render() { 27 | if (this.state.md && !this.state.error) { 28 | return ( 29 |
    33 | ); 34 | } else if (this.state.error) { 35 | return this.props.unloader ||
    Error: couldn't load source
    ; 36 | } else { 37 | return this.props.loader; 38 | } 39 | } 40 | } 41 | 42 | Md.propTypes = { 43 | loader: PropTypes.func, 44 | unloader: PropTypes.func.isRequired, 45 | path: PropTypes.string.isRequired, 46 | }; 47 | 48 | export default Md; 49 | -------------------------------------------------------------------------------- /src/components/atoms/CoeventIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CoeventIcon = ({ isEnabled, toggleMapViews }) => { 4 | return ( 5 | 40 | ); 41 | }; 42 | 43 | export default CoeventIcon; 44 | -------------------------------------------------------------------------------- /src/components/space/carto/atoms/Regions.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Portal } from "react-portal"; 3 | 4 | function MapRegions({ svg, regions, projectPoint, styles }) { 5 | function renderRegion(region) { 6 | const lineCoords = []; 7 | const points = region.points.map(projectPoint); 8 | 9 | points.forEach((p1, idx) => { 10 | if (idx < region.points.length - 1) { 11 | const p2 = points[idx + 1]; 12 | lineCoords.push({ 13 | x1: p1.x, 14 | y1: p1.y, 15 | x2: p2.x, 16 | y2: p2.y, 17 | }); 18 | } 19 | }); 20 | 21 | return lineCoords.map((coords) => { 22 | const regionstyles = 23 | region.name in styles ? styles[region.name] : styles.default; 24 | 25 | return ( 26 | 32 | ); 33 | }); 34 | } 35 | 36 | if (!regions || !regions.length) return null; 37 | 38 | return ( 39 | 40 | 41 | {regions.map(renderRegion)} 42 | 43 | 44 | ); 45 | } 46 | 47 | export default MapRegions; 48 | -------------------------------------------------------------------------------- /src/components/space/carto/atoms/SatelliteOverlayToggle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import copy from "../../../../common/data/copy.json"; 3 | import { language } from "../../../../common/utilities"; 4 | import mapImg from "../../../../assets/satelliteoverlaytoggle/map.png"; 5 | import satImg from "../../../../assets/satelliteoverlaytoggle/sat.png"; 6 | 7 | const SatelliteOverlayToggle = ({ 8 | switchToSatellite, 9 | reset, 10 | isUsingSatellite, 11 | }) => { 12 | return ( 13 |
    14 | {isUsingSatellite ? ( 15 | 22 | ) : ( 23 | 30 | )} 31 |
    32 | ); 33 | }; 34 | 35 | export default SatelliteOverlayToggle; 36 | -------------------------------------------------------------------------------- /src/scss/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | background: #000000; 3 | position: fixed; 4 | padding: 10px; 5 | z-index: 10; 6 | top: 10px; 7 | right: 10px; 8 | height: 40px; 9 | width: 240px; 10 | box-sizing: border-box; 11 | text-overflow: ellipsis; 12 | box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22); 13 | cursor: pointer; 14 | 15 | .header-title { 16 | a { 17 | color: darken($offwhite, 5%); 18 | font-size: $xlarge; 19 | letter-spacing: 0.1em; 20 | float: left; 21 | text-transform: uppercase; 22 | } 23 | 24 | p { 25 | margin: 0; 26 | } 27 | } 28 | 29 | .side-menu-burg { 30 | right: 10px; 31 | span, 32 | span::before, 33 | span::after { 34 | background: $midwhite; 35 | } 36 | } 37 | 38 | &:hover { 39 | .side-menu-burg { 40 | span { 41 | transition: 0.2s ease; 42 | background: $offwhite; 43 | } 44 | span::before { 45 | transition: 0.2s ease; 46 | top: -6px; 47 | background: $offwhite; 48 | } 49 | 50 | span::after { 51 | transition: 0.2s ease; 52 | bottom: -6px; 53 | background: $offwhite; 54 | } 55 | } 56 | .header-title a { 57 | transition: 0.2s ease; 58 | color: $offwhite; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/controls/atoms/Text.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const CardText = ({ title, value, hoverValue = null }) => { 4 | const [showHover, setShowHover] = useState(false); 5 | 6 | return ( 7 |
    8 | {title ?

    {title}

    : null} 9 |
    14 |
    hoverValue && setShowHover(true)} 16 | onMouseOut={() => hoverValue && setShowHover(false)} 17 | > 18 | {showHover ? ( 19 | 25 | {hoverValue} 26 | 27 | ) : ( 28 |
    36 | {value} 37 |
    38 | )} 39 |
    40 | {/* {!showHover && value} */} 41 |
    42 |
    43 | ); 44 | }; 45 | 46 | export default CardText; 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Environment 7 | ----------- 8 | 9 | * Your version (in package.json) or git commit hash 10 | * Your operating system and version: 11 | 12 | 13 | 14 | Steps to reproduce (for bugs only) 15 | ----------------------------- 16 | 17 | 18 | 19 | 1. 20 | 2. 21 | 3. 22 | 23 | Current Behavior 24 | ---------------- 25 | 26 | 27 | 28 | 29 | Expected Behavior 30 | ----------------- 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const camelcase = require("camelcase"); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/space/carto/atoms/SelectedEvents.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Portal } from "react-portal"; 3 | import colors from "../../../../common/global"; 4 | import hash from "object-hash"; 5 | 6 | class MapSelectedEvents extends React.Component { 7 | renderMarker(marker) { 8 | const { x, y } = this.props.projectPoint([ 9 | marker.latitude, 10 | marker.longitude, 11 | ]); 12 | const styles = this.props.styles; 13 | const r = marker.radius ? marker.radius + 5 : 24; 14 | return ( 15 | 20 | 31 | 32 | ); 33 | } 34 | 35 | render() { 36 | return ( 37 | 38 | {this.props.selected.map((s) => this.renderMarker(s))} 39 | 40 | ); 41 | } 42 | } 43 | export default MapSelectedEvents; 44 | -------------------------------------------------------------------------------- /src/components/controls/BottomActions.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import SitesIcon from "../atoms/SitesIcon"; 4 | import CoverIcon from "../atoms/CoverIcon"; 5 | // import InfoIcon from "../atoms/InfoIcon"; 6 | 7 | function BottomActions(props) { 8 | function renderToggles() { 9 | return ( 10 | <> 11 |
    12 | {props.features.USE_SITES ? ( 13 | 17 | ) : null} 18 |
    19 | {/* , 20 |
    21 | 25 |
    26 | , */} 27 |
    28 | {props.features.USE_COVER ? ( 29 | 30 | ) : null} 31 |
    32 |
    Made with TimeMap
    Free software from
    Forensic Architecture
    33 | 34 | ); 35 | } 36 | 37 | return
    {renderToggles()}
    ; 38 | } 39 | 40 | export default BottomActions; 41 | -------------------------------------------------------------------------------- /src/components/time/atoms/ZoomControls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DEFAULT_ZOOM_LEVELS = [ 4 | { label: "20 years", duration: 10512000 }, 5 | { label: "2 years", duration: 1051200 }, 6 | { label: "3 months", duration: 129600 }, 7 | { label: "3 days", duration: 4320 }, 8 | { label: "12 hours", duration: 720 }, 9 | { label: "1 hour", duration: 60 }, 10 | ]; 11 | 12 | function zoomIsActive(duration, extent, max) { 13 | if (duration >= max && extent >= max) { 14 | return true; 15 | } 16 | return duration === extent; 17 | } 18 | 19 | const TimelineZoomControls = ({ extent, zoomLevels, dims, onApplyZoom }) => { 20 | function renderZoom(zoom, idx) { 21 | const max = zoomLevels.reduce((acc, vl) => 22 | acc.duration < vl.duration ? vl : acc 23 | ); 24 | const isActive = zoomIsActive(zoom.duration, extent, max.duration); 25 | return ( 26 | onApplyZoom(zoom)} 31 | key={idx} 32 | > 33 | {zoom.label} 34 | 35 | ); 36 | } 37 | 38 | if (zoomLevels.length === 0) { 39 | zoomLevels = DEFAULT_ZOOM_LEVELS; 40 | } 41 | return ( 42 | 43 | {zoomLevels.map((z, idx) => renderZoom(z, idx))} 44 | 45 | ); 46 | }; 47 | 48 | export default TimelineZoomControls; 49 | -------------------------------------------------------------------------------- /src/components/controls/FullScreenToggle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import screenfull from "screenfull"; 3 | import { ToolbarButton } from "./atoms/ToolbarButton"; 4 | import copy from "../../common/data/copy.json"; 5 | 6 | export class FullscreenToggle extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.onFullscreenStateChange = this.onFullscreenStateChange.bind(this); 11 | 12 | this.state = { 13 | isFullscreen: screenfull.isFullscreen, 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | screenfull.on("change", this.onFullscreenStateChange); 19 | } 20 | 21 | componentWillUnmount() { 22 | screenfull.off("change", this.onFullscreenStateChange); 23 | } 24 | 25 | onFullscreenStateChange(evt) { 26 | this.setState({ isFullscreen: screenfull.isFullscreen }); 27 | } 28 | 29 | onToggleFullscreen() { 30 | screenfull.toggle().catch(console.warn); 31 | } 32 | 33 | render() { 34 | if (!screenfull.isEnabled) return null; 35 | 36 | const { language } = this.props; 37 | const { isFullscreen } = this.state; 38 | 39 | return ( 40 | 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import store from "./store"; 5 | import App from "./components/App"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById("explore-app") 12 | ); 13 | 14 | // Expressions from https://exceptionshub.com/how-to-detect-safari-chrome-ie-firefox-and-opera-browser.html 15 | 16 | /* eslint-disable */ 17 | // Opera 8.0+ 18 | const isOpera = 19 | (!!window.opr && !!opr.addons) || 20 | !!window.opera || 21 | navigator.userAgent.indexOf(" OPR/") >= 0; 22 | // Firefox 1.0+ 23 | const isFirefox = typeof InstallTrigger !== "undefined"; 24 | // Safari 3.0+ "[object HTMLElementConstructor]" 25 | const isSafari = 26 | /constructor/i.test(window.HTMLElement) || 27 | (function (p) { 28 | return p.toString() === "[object SafariRemoteNotification]"; 29 | })( 30 | !window["safari"] || 31 | (typeof safari !== "undefined" && safari.pushNotification) 32 | ); 33 | // Internet Explorer 6-11 34 | const isIE = /* @cc_on!@ */ false || !!document.documentMode; 35 | // Edge 20+ 36 | const isEdge = !isIE && !!window.StyleMedia; 37 | // Chrome 1+ 38 | const isChrome = !!window.chrome && !!window.chrome.webstore; 39 | // Blink engine detection 40 | const isBlink = (isChrome || isOpera) && !!window.CSS; 41 | 42 | if (isEdge || isIE) { 43 | alert( 44 | "Please view this website in Opera for best viewing. It is untested in your browser." 45 | ); 46 | } 47 | /* eslint-enable */ 48 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 && 45 | argv.indexOf('--watchAll=false') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /docs/mapbox.md: -------------------------------------------------------------------------------- 1 | 2 | # Timemap and custom Mapbox maps 3 | 4 | You can use satellite images and maps from [https://www.mapbox.com/](https://www.mapbox.com/) to customise the map in Timemap. 5 | 6 | Sign up for an account at [https://www.mapbox.com/](https://www.mapbox.com/) and use Mapbox's studio to create a custom map. To use the map you need to configure Timemap with the following: 7 | 8 | ## Token 9 | 10 | To access your Mapbox account Timemap needs an access token that you create under your Mapbox account. 11 | 12 | Sign in to Mapbox and then navigate to: 13 | 14 | Account > Access Tokens 15 | 16 | * Create a Token - add a token and optionally restrict it to the url of your timemap instance. 17 | * Add it to Timemap - copy the token and then add it to Timemap's `config.js` at the top level: 18 | 19 | ``` 20 | MAPBOX_TOKEN: 'mapbox_token', 21 | ``` 22 | 23 | Timemap can now access your account but you need to associate any maps you want to use with that account using Mapbox Studio. Once you have done that you can then reference the Mapbox Map id. 24 | 25 | ## Mapbox Map Id 26 | 27 | To reference a map you have created in Mapbox Studio you need the map's id which is available under 'share' > 28 | 29 | * Style URL (which looks like this: `mapbox://styles/your-mapbox-account-name/x5678-map-id`) 30 | 31 | Now go back to `config.js` and under the UI settings add (don't copy the 'mapbox://styles/' bit): 32 | 33 | ``` 34 | ui: { 35 | tiles: 'your-mapbox-account-name/x5678-map-id', 36 | ``` 37 | 38 | If you restart your time map instance you should now see the Mapbox map. 39 | -------------------------------------------------------------------------------- /test/server_process.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | const childProcess = require('child_process') 3 | const http = require('http') 4 | 5 | const SERVER_LAUNCH_WAIT_TIME = 10 * 1000 6 | 7 | let serverProc = null 8 | let serverExited = false 9 | 10 | test.before.cb(t => { 11 | console.log('launching server...') 12 | serverProc = childProcess.spawn('yarn', ['dev'], { 13 | cwd: '.', 14 | stdio: 'ignore' 15 | }) 16 | 17 | serverProc.on('exit', function (code, signal) { 18 | serverExited = true 19 | }) 20 | 21 | setTimeout(t.end, SERVER_LAUNCH_WAIT_TIME) 22 | }) 23 | 24 | test.after(function () { 25 | console.log('killing server...') 26 | serverProc.kill('SIGKILL') 27 | }) 28 | 29 | test('should launch', t => { 30 | t.false(serverExited) 31 | }) 32 | 33 | const urls = [ 34 | '/', 35 | 'js/index.bundle.js' 36 | ] 37 | 38 | urls.forEach(function (url) { 39 | test.cb('should respond to request for "' + url + '"', t => { 40 | http.get({ 41 | hostname: 'localhost', 42 | port: 8080, 43 | path: '/', 44 | agent: false 45 | }, function (res) { 46 | let resultData = '' 47 | 48 | if (res.statusCode !== 200) { 49 | t.fail('Server response was not 200.') 50 | } else { 51 | res.on('data', function (data) { resultData += data }) 52 | 53 | res.on('end', function () { 54 | if (resultData.length > 0) { 55 | t.pass() 56 | } else { 57 | t.fail('Server returned no data.') 58 | } 59 | }) 60 | } 61 | 62 | t.end() 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/scss/popup.scss: -------------------------------------------------------------------------------- 1 | .popup { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 15px; 5 | border: 0; 6 | opacity: 0; 7 | border-radius: 2px; 8 | transition: 0.2 ease; 9 | background: rgba(0, 0, 0, 0.9); 10 | transition: 0.4s ease; 11 | box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22); 12 | 13 | &:hover { 14 | transition: 0.4s ease; 15 | box-shadow: 0 29px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22); 16 | } 17 | 18 | .card-tophalf { 19 | height: 100px; 20 | 21 | .left { 22 | float: left; 23 | width: 120px; 24 | padding-right: 5px; 25 | box-sizing: border-box; 26 | border-right: 1px dotted $midwhite; 27 | } 28 | .right { 29 | float: left; 30 | width: 225px; 31 | padding-left: 5px; 32 | height: 90px; 33 | overflow: hidden; 34 | } 35 | } 36 | 37 | .filter, 38 | p.see-more { 39 | cursor: pointer; 40 | 41 | &:hover { 42 | color: $yellow; 43 | } 44 | } 45 | 46 | p { 47 | margin: 5px 0 0 0; 48 | } 49 | 50 | .timestamp { 51 | text-transform: uppercase; 52 | font-size: $xlarge; 53 | margin-top: 0; 54 | } 55 | 56 | .location { 57 | font-size: $normal; 58 | color: $offwhite; 59 | } 60 | 61 | .estimated-timestamp { 62 | margin-top: 3px; 63 | margin-left: 3px; 64 | font-size: $xsmall; 65 | color: $midwhite; 66 | text-transform: lowercase; 67 | } 68 | 69 | .summary { 70 | max-height: 200px; 71 | text-overflow: ellipsis; 72 | overflow: scroll; 73 | font-weight: 500; 74 | } 75 | 76 | .source { 77 | text-align: right; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/atoms/ColoredMarkers.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getCoordinatesForPercent } from "../../common/utilities"; 3 | 4 | function ColoredMarkers({ radius, colorPercentMap, styles, className }) { 5 | let cumulativeAngleSweep = 0; 6 | const colors = Object.keys(colorPercentMap); 7 | 8 | return ( 9 | <> 10 | {colors.map((color, idx) => { 11 | const colorPercent = colorPercentMap[color]; 12 | 13 | const [startX, startY] = getCoordinatesForPercent( 14 | radius, 15 | cumulativeAngleSweep 16 | ); 17 | 18 | cumulativeAngleSweep += colorPercent; 19 | 20 | const [endX, endY] = getCoordinatesForPercent( 21 | radius, 22 | cumulativeAngleSweep 23 | ); 24 | // if the slices are less than 2, take the long arc 25 | const largeArcFlag = colors.length === 1 || colorPercent > 0.5 ? 1 : 0; 26 | 27 | // create an array and join it just for code readability 28 | const arc = [ 29 | `M ${startX} ${startY}`, // Move 30 | `A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc 31 | "L 0 0 ", // Line 32 | `L ${startX} ${startY} Z`, // Line 33 | ].join(" "); 34 | 35 | const extraStyles = { 36 | ...styles, 37 | fill: color, 38 | }; 39 | 40 | return ( 41 | 48 | ); 49 | })} 50 | 51 | ); 52 | } 53 | 54 | export default ColoredMarkers; 55 | -------------------------------------------------------------------------------- /src/reducers/validate/eventSchema.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | function joiFromCustom(custom) { 4 | const output = {}; 5 | custom.forEach((field) => { 6 | if (field.kind === "text" || field.kind === "link") { 7 | output[field.key] = Joi.string().allow(""); 8 | } 9 | if (field.kind === "list") { 10 | output[field.key] = Joi.array().allow(""); 11 | } 12 | }); 13 | return output; 14 | } 15 | 16 | function createEventSchema(custom) { 17 | return Joi.object() 18 | .keys({ 19 | id: Joi.string().allow(""), 20 | civId: Joi.string().allow(""), 21 | description: Joi.string().allow("").required(), 22 | date: Joi.string().allow(""), 23 | time: Joi.string().allow(""), 24 | time_precision: Joi.string().allow(""), 25 | 26 | /* map */ 27 | location: Joi.string().allow(""), 28 | latitude: Joi.string().allow(""), 29 | longitude: Joi.string().allow(""), 30 | /* space */ 31 | x: Joi.string().allow(""), 32 | y: Joi.string().allow(""), 33 | z: Joi.string().allow(""), 34 | 35 | type: Joi.string().allow(""), 36 | category: Joi.string().allow(""), 37 | category_full: Joi.string().allow(""), 38 | associations: Joi.array().default([]), 39 | sources: Joi.array(), 40 | comments: Joi.string().allow(""), 41 | time_display: Joi.string().allow(""), 42 | // nested 43 | narrative___stepStyles: Joi.array(), 44 | shape: Joi.string().allow(""), 45 | colour: Joi.string().allow(""), 46 | ...joiFromCustom(custom), 47 | }) 48 | .and("latitude", "longitude") 49 | .or("date", "latitude"); 50 | } 51 | 52 | export default createEventSchema; 53 | -------------------------------------------------------------------------------- /src/scss/notification.scss: -------------------------------------------------------------------------------- 1 | @import "burger"; 2 | 3 | .notification-wrapper { 4 | top: 60px; 5 | right: 60px; 6 | width: 400px; 7 | height: auto; 8 | position: absolute; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | .notification { 14 | width: 100%; 15 | min-height: 40px; 16 | box-shadow: 10px -10px 38px rgba(0, 0, 0, 0.3), 17 | 10px 15px 12px rgba(0, 0, 0, 0.22); 18 | color: $darkgrey; 19 | background: $offwhite; 20 | border-radius: 5px; 21 | border: 3px solid $offwhite; 22 | padding: 20px; 23 | margin-bottom: 10px; 24 | box-sizing: border-box; 25 | font-size: $large; 26 | transition: opacity 0.5s ease 0.1s, z-index 0.1s ease 0s; 27 | opacity: 1; 28 | z-index: $overheader; 29 | cursor: pointer; 30 | 31 | &:hover { 32 | background: lighten($offwhite, 5%); 33 | transition: background-color 0.4s; 34 | } 35 | 36 | &.hidden { 37 | transition: 0.5s ease; 38 | opacity: 0; 39 | } 40 | 41 | .side-menu-burg { 42 | position: absolute; 43 | right: 8px; 44 | top: 10px; 45 | } 46 | 47 | .message { 48 | display: inline-block; 49 | 50 | &.error { 51 | color: red; 52 | } 53 | &.warning { 54 | color: orange; 55 | } 56 | &.good { 57 | color: green; 58 | } 59 | &.neutral { 60 | color: $darkgrey; 61 | } 62 | } 63 | 64 | .details { 65 | overflow: hidden; 66 | display: flex; 67 | flex-direction: column; 68 | border-radius: 3px; 69 | margin-top: 10px; 70 | padding: 10px; 71 | background: $darkgrey; 72 | color: $offwhite; 73 | font-family: monospace; 74 | 75 | &.true { 76 | height: auto; 77 | transition: height 0.4s, margin 0.4s; 78 | } 79 | 80 | &.false { 81 | height: 0; 82 | padding: 0; 83 | margin: 0; 84 | transition: height 0.4s, margin 0.4s; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/components/space/carto/atoms/__tests__/SatelliteOverlayToggle.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { fireEvent, render, screen } from "@testing-library/react"; 3 | import SatelliteOverlayToggle from "../SatelliteOverlayToggle"; 4 | import "@testing-library/jest-dom"; 5 | 6 | describe("", () => { 7 | it("shows the option to switch to satellite by default", () => { 8 | render( 9 | 10 | ); 11 | expect(screen.getByRole("button", { name: /sat/i })).toBeTruthy(); 12 | }); 13 | 14 | it("shows the option to switch to map when satellite selected", () => { 15 | render( 16 | 21 | ); 22 | expect(screen.getByRole("button", { name: /map/i })).toBeTruthy(); 23 | }); 24 | 25 | it("calls the reset function when switching to the default overlay", () => { 26 | const mockReset = jest.fn(); 27 | const mockSat = jest.fn(); 28 | render( 29 | 34 | ); 35 | const btn = screen.getByRole("button", { name: /map/i }); 36 | fireEvent.click(btn); 37 | expect(mockReset).toHaveBeenCalledTimes(1); 38 | expect(mockSat).not.toHaveBeenCalled(); 39 | }); 40 | 41 | it("calls the switchToSatellite function when switching to the satellite overlay", () => { 42 | const mockReset = jest.fn(); 43 | const mockSat = jest.fn(); 44 | render( 45 | 46 | ); 47 | const btn = screen.getByRole("button", { name: /sat/i }); 48 | fireEvent.click(btn); 49 | expect(mockSat).toHaveBeenCalledTimes(1); 50 | expect(mockReset).not.toHaveBeenCalled(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "GT-Zirkon"; 3 | src: url(../assets/fonts/timemapfont.woff); // a Lato woff by default 4 | } 5 | 6 | $event_default: red; 7 | 8 | $offwhite: #efefef; 9 | $offwhite-transparent: rgba(239, 239, 239, 0.9); 10 | $lightwhite: #dfdfdf; 11 | $midwhite: #a0a0a0; 12 | $darkwhite: darken($midwhite, 15%); 13 | $yellow: #eb443e; // #ffd800; 14 | $red: rgb(233, 0, 19); 15 | $green: rgb(61, 241, 79); 16 | $midgrey: rgb(44, 44, 44); 17 | $darkgrey: #232323; 18 | $black: #000000; 19 | $black-transparent: rgba(0, 0, 0, 0.7); 20 | 21 | // Category colors 22 | $default: red; 23 | $alpha: #00ff00; 24 | $beta: #ff00ff; 25 | $other: yellow; 26 | 27 | .default { 28 | background: $default; 29 | } 30 | .other { 31 | background: $other; 32 | } 33 | .alpha { 34 | background: $alpha; 35 | } 36 | .beta { 37 | background: $beta; 38 | } 39 | 40 | $mainfont: "GT-Zirkon", "Lato", Helvetica, sans-serif; 41 | 42 | // Font sizes 43 | $xsmall: 10px; //0.7em; 44 | $small: 11px; //0.9em; 45 | $normal: 12px; //1em; 46 | $large: 14px; //1.1em; 47 | $xlarge: 16px; //1.2em; 48 | $xxlarge: 20px; //1.4em; 49 | $xxxlarge: 32px; 50 | 51 | // z-index levels 52 | $final-level: 10000; 53 | $loading-overlay: 500; 54 | $overheader: 100; 55 | $header: 10; 56 | $map-overlay: 2; 57 | $map: 1; 58 | $scene: 1; 59 | $hidden: -1; 60 | $timeline: 13; 61 | 62 | // platform-specific sizes 63 | $infopopup-width: 400px; 64 | $infopopup-left: 122px; 65 | $infopopup-bottom: 180px; 66 | $card-width: 500px; 67 | $card-right: 2px; 68 | $narrative-info-height: 205px; 69 | $narrative-info-desc-height: 153px; 70 | $timeline-height: 250px; 71 | $toolbar-width: 110px; 72 | 73 | $panel-width: 1000px; 74 | $panel-height: 1000px; 75 | $vimeo-width: $panel-width - 100; 76 | $vimeo-height: $panel-height / 2; 77 | $banner-height: 100px; 78 | $padding: 20px; 79 | $header-inset: 10px; 80 | 81 | // CSS variables (for React access) 82 | :root { 83 | --toolbar-width: 110px; 84 | --error-red: #eb443e; 85 | } 86 | -------------------------------------------------------------------------------- /src/components/controls/atoms/Button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | /** 5 | * Primary UI component for user interaction 6 | */ 7 | export const Button = ({ 8 | primary, 9 | backgroundColor, 10 | borderRadius, 11 | size, 12 | label, 13 | normalCursor, 14 | ...props 15 | }) => { 16 | const mode = primary ? "button--primary" : "button--secondary"; 17 | return ( 18 | 31 | ); 32 | }; 33 | 34 | Button.propTypes = { 35 | /** 36 | * Is this the principal call to action on the page? 37 | */ 38 | primary: PropTypes.bool, 39 | /** 40 | * What background color to use 41 | */ 42 | backgroundColor: PropTypes.string, 43 | /** 44 | * How much rounded are they? 45 | */ 46 | borderRadius: PropTypes.string, 47 | /** 48 | * How large should the button be? 49 | */ 50 | size: PropTypes.oneOf(["small", "medium", "large"]), 51 | /** 52 | * Button contents 53 | */ 54 | label: PropTypes.string.isRequired, 55 | /** 56 | * Optional click handler 57 | */ 58 | onClick: PropTypes.func, 59 | }; 60 | 61 | Button.defaultProps = { 62 | backgroundColor: "red", 63 | borderRadius: "0%", 64 | primary: false, 65 | size: "medium", 66 | onClick: undefined, 67 | }; 68 | 69 | const CardButton = ({ 70 | text, 71 | color = "#000", 72 | onClick = () => {}, 73 | normalCursor, 74 | }) => ( 75 | 63 | {this.renderNotificationContent(notification)} 64 |
    65 | ); 66 | })} 67 |
    68 | ); 69 | } 70 | return
    ; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/controls/atoms/SearchRow.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SearchRow = ({ query, eventObj, onSearchRowClick }) => { 4 | const { description, location, date } = eventObj; 5 | function getHighlightedText(text, highlight) { 6 | // Split text on highlight term, include term itself into parts, ignore case 7 | const parts = text.split(new RegExp(`(${highlight})`, "gi")); 8 | return ( 9 | 10 | {parts.map((part) => 11 | part.toLowerCase() === highlight.toLowerCase() ? ( 12 | 13 | {part} 14 | 15 | ) : ( 16 | part 17 | ) 18 | )} 19 | 20 | ); 21 | } 22 | 23 | function getShortDescription(text, searchQuery) { 24 | const regexp = new RegExp( 25 | `(([^ ]* ){0,6}[a-zA-Z]*${searchQuery.toLowerCase()}[a-zA-Z]*( [^ ]*){0,5})`, 26 | "gm" 27 | ); 28 | const parts = text.toLowerCase().match(regexp); 29 | for (let x = 0; x < (parts ? parts.length : 0); x++) { 30 | parts[x] = "..." + parts[x]; 31 | } 32 | const firstLine = [text.match("(([^ ]* ){0,10})", "m")[0]]; 33 | return parts || firstLine; 34 | } 35 | 36 | return ( 37 |
    onSearchRowClick([eventObj])}> 38 |
    39 |
    40 | event 41 |

    {getHighlightedText(date, query)}

    42 |
    43 |
    44 | location_on 45 |

    {getHighlightedText(location, query)}

    46 |
    47 |
    48 |

    49 | {getShortDescription(description, query).map((match) => { 50 | return ( 51 | 52 | {getHighlightedText(match, query)}... 53 |
    54 |
    55 | ); 56 | })} 57 |

    58 |
    59 | ); 60 | }; 61 | 62 | export default SearchRow; 63 | -------------------------------------------------------------------------------- /config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /src/scss/loading.scss: -------------------------------------------------------------------------------- 1 | .loading-overlay { 2 | font-weight: 300; 3 | width: 100%; 4 | height: 100%; 5 | position: absolute; 6 | background: rgba(0, 0, 0, 0.9); 7 | transition: 0.4s ease; 8 | z-index: $loading-overlay; 9 | opacity: 1; 10 | 11 | .loading-wrapper { 12 | position: fixed; 13 | left: 50%; 14 | top: 40%; 15 | text-align: center; 16 | width: 100%; 17 | margin: 0 0 0 -50%; 18 | height: 100%; 19 | opacity: 1; 20 | 21 | span { 22 | color: $offwhite; 23 | letter-spacing: 0.1em; 24 | text-transform: uppercase; 25 | } 26 | } 27 | 28 | &.hidden { 29 | transition: opacity 0.4s ease, z-index 0.1s 0.4s; 30 | opacity: 0; 31 | z-index: $hidden; 32 | } 33 | } 34 | 35 | /* 36 | https://github.com/tobiasahlin/SpinKit/blob/master/LICENSE 37 | */ 38 | .spinner { 39 | width: 40px; 40 | height: 40px; 41 | 42 | position: relative; 43 | margin: 10px auto; 44 | 45 | &.small { 46 | width: 15px; 47 | height: 15px; 48 | margin: 5px 20px 5px 10px; 49 | } 50 | } 51 | 52 | .double-bounce, 53 | .double-bounce-overlay { 54 | width: 100%; 55 | height: 100%; 56 | border-radius: 50%; 57 | background-color: $offwhite; 58 | opacity: 0.6; 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | 63 | -webkit-animation: sk-bounce 3s infinite ease-in-out; 64 | animation: sk-bounce 3s infinite ease-in-out; 65 | } 66 | 67 | .double-bounce-overlay { 68 | -webkit-animation-delay: -1s; 69 | animation-delay: -1s; 70 | background-color: black; 71 | } 72 | 73 | @-webkit-keyframes sk-bounce { 74 | 0%, 75 | 100% { 76 | -webkit-transform: scale(0.3); 77 | } 78 | 50% { 79 | -webkit-transform: scale(1); 80 | } 81 | } 82 | 83 | @keyframes sk-bounce { 84 | 0%, 85 | 100% { 86 | transform: scale(0.3); 87 | -webkit-transform: scale(0.3); 88 | } 89 | 50% { 90 | transform: scale(1); 91 | -webkit-transform: scale(1); 92 | } 93 | } 94 | 95 | .fixedTooSmallMessage { 96 | position: absolute; 97 | top: 0; 98 | color: white; 99 | padding: 10px; 100 | } 101 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Civilian Harm in Ukraine 7 | 8 | 9 | 10 | 11 | 12 | 22 | 23 | 24 | 25 | 26 | 43 |
    44 |
    45 |
    46 |
    47 |
    48 | This platform is unsuitable for mobile. Please revisit on a desktop. 49 |
    50 |
    51 | 52 | 53 | -------------------------------------------------------------------------------- /src/scss/search.scss: -------------------------------------------------------------------------------- 1 | #search-bar-icon-container { 2 | position: absolute; 3 | background-color: black; 4 | color: #a0a0a0; 5 | border: #a0a0a0 solid 0.1px; 6 | top: 10px; 7 | margin-left: 10px; 8 | height: 24px; 9 | padding: 10px; 10 | &:hover { 11 | cursor: pointer; 12 | color: white; 13 | } 14 | } 15 | 16 | .search-bar-overlay { 17 | background-color: black; 18 | height: 100vh; 19 | width: 400px; 20 | position: absolute; 21 | transition: 0.2s ease; 22 | } 23 | 24 | .search-bar-input { 25 | width: 300px; 26 | margin: 20px; 27 | line-height: 40px; 28 | font-size: 15px; 29 | color: gray; 30 | padding-left: 15px; 31 | background: black; 32 | border: 1px solid #a0a0a0; 33 | &:focus { 34 | outline: none; 35 | } 36 | } 37 | 38 | #close-search-overlay { 39 | color: #a0a0a0; 40 | vertical-align: middle; 41 | font-size: 30px; 42 | transition: 0.2s ease; 43 | &:hover { 44 | color: white; 45 | cursor: pointer; 46 | } 47 | } 48 | 49 | .folded { 50 | left: -400px; 51 | transition: 0.2s ease; 52 | } 53 | 54 | .search-outer-container { 55 | position: absolute; 56 | left: 110px; 57 | &.narrative-mode { 58 | left: 0; 59 | } 60 | } 61 | 62 | .search-row { 63 | color: black; 64 | padding-left: 15px; 65 | padding-right: 15px; 66 | padding-top: 10px; 67 | padding-bottom: 10px; 68 | background-color: #dfdfdf; 69 | transition: background-color 0.4s; 70 | border-bottom: 1px black solid; 71 | border-top: 1px black solid; 72 | font-size: 14px; 73 | opacity: 0.9; 74 | &:hover { 75 | transition: background-color 0.4s; 76 | background-color: white; 77 | cursor: pointer; 78 | } 79 | } 80 | 81 | .search-row > p { 82 | margin: 0; 83 | } 84 | 85 | .search-results { 86 | height: calc(100% - 332px); 87 | overflow: auto; 88 | } 89 | 90 | div.location-date-container { 91 | margin-top: 10px; 92 | margin-bottom: 10px; 93 | } 94 | 95 | div.location-date-container > div { 96 | width: 50%; 97 | display: inline-block; 98 | vertical-align: top; 99 | } 100 | 101 | div.location-date-container > div > p { 102 | display: inline; 103 | line-height: 17px; 104 | vertical-align: top; 105 | } 106 | 107 | div.location-date-container > div > i { 108 | font-size: 12px; 109 | margin-right: 5px; 110 | } 111 | -------------------------------------------------------------------------------- /src/components/time/Axis.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as d3 from "d3"; 3 | import { setD3Locale } from "../../common/utilities"; 4 | 5 | const TEXT_HEIGHT = 15; 6 | setD3Locale(d3); 7 | class TimelineAxis extends React.Component { 8 | constructor() { 9 | super(); 10 | this.xAxis0Ref = React.createRef(); 11 | this.xAxis1Ref = React.createRef(); 12 | this.state = { 13 | isInitialized: false, 14 | }; 15 | } 16 | 17 | componentDidUpdate() { 18 | let fstFmt, sndFmt; 19 | 20 | // 10yrs 21 | if (this.props.extent > 5256000) { 22 | fstFmt = "%Y"; 23 | sndFmt = ""; 24 | // 1yr 25 | } else if (this.props.extent > 43200) { 26 | sndFmt = "%d %b"; 27 | fstFmt = ""; 28 | } else { 29 | sndFmt = "%d %b"; 30 | // fstFmt = "%H:%M"; 31 | fstFmt = ""; 32 | } 33 | 34 | const { marginTop, contentHeight } = this.props.dims; 35 | if (this.props.scaleX) { 36 | this.x0 = d3 37 | .axisBottom(this.props.scaleX) 38 | .ticks(this.props.ticks) 39 | .tickPadding(0) 40 | .tickSize(contentHeight - TEXT_HEIGHT - marginTop) 41 | .tickFormat(d3.timeFormat(fstFmt)); 42 | 43 | this.x1 = d3 44 | .axisBottom(this.props.scaleX) 45 | .ticks(this.props.ticks) 46 | .tickPadding(marginTop) 47 | .tickSize(0) 48 | .tickFormat(d3.timeFormat(sndFmt)); 49 | 50 | if (!this.state.isInitialized) this.setState({ isInitialized: true }); 51 | } 52 | 53 | if (this.state.isInitialized) { 54 | d3.select(this.xAxis0Ref.current) 55 | .transition() 56 | .duration(this.props.transitionDuration) 57 | .call(this.x0); 58 | 59 | d3.select(this.xAxis1Ref.current) 60 | .transition() 61 | .duration(this.props.transitionDuration) 62 | .call(this.x1); 63 | } 64 | } 65 | 66 | render() { 67 | return ( 68 | <> 69 | 75 | 81 | 82 | ); 83 | } 84 | } 85 | 86 | export default TimelineAxis; 87 | -------------------------------------------------------------------------------- /src/scss/_burger.scss: -------------------------------------------------------------------------------- 1 | // Burger transition 2 | .side-menu-burg { 3 | position: absolute; 4 | overflow: hidden; 5 | float: right; 6 | margin: 0; 7 | padding: 0; 8 | width: 20px; 9 | height: 20px; 10 | appearance: none; 11 | box-shadow: none; 12 | border-radius: none; 13 | border: none; 14 | cursor: pointer; 15 | background: none; 16 | 17 | &.hidden { 18 | display: none; 19 | } 20 | 21 | span { 22 | display: block; 23 | position: absolute; 24 | top: 9px; 25 | left: 0px; 26 | right: 0px; 27 | height: 2px; 28 | background: $offwhite; 29 | border-radius: 4px; 30 | } 31 | 32 | span::before, 33 | span::after { 34 | position: absolute; 35 | display: block; 36 | left: 0; 37 | width: 100%; 38 | height: 2px; 39 | background: $offwhite; 40 | border-radius: 4px; 41 | content: ""; 42 | transition-duration: 0.2s, 0.2s; 43 | transition-delay: 0.2s, 0s; 44 | } 45 | 46 | span::before { 47 | transition-property: top, transform; 48 | top: -8px; 49 | } 50 | 51 | span::after { 52 | transition-property: bottom, transform; 53 | bottom: -8px; 54 | } 55 | 56 | &:hover { 57 | span::before { 58 | top: -6px; 59 | } 60 | 61 | span::after { 62 | bottom: -6px; 63 | } 64 | } 65 | 66 | &.is-active { 67 | span { 68 | background: $midwhite; 69 | transform: rotate(45deg); 70 | transition-delay: 0s, 0.2s; 71 | } 72 | 73 | span::before, 74 | span::after { 75 | background: $midwhite; 76 | transition-delay: 0s, 0.2s; 77 | } 78 | 79 | span::before { 80 | top: 0; 81 | transform: rotate(0deg); 82 | -webkit-transform: rotate(0deg); 83 | } 84 | 85 | span::after { 86 | bottom: 0; 87 | transform: rotate(-90deg); 88 | -webkit-transform: rotate(-90deg); 89 | } 90 | 91 | &:hover { 92 | span, 93 | span::before, 94 | span::after { 95 | transition: 0.2s ease; 96 | background: $offwhite; 97 | } 98 | } 99 | 100 | &.over-white:hover { 101 | span, 102 | span:before, 103 | span:after { 104 | transition: 0.2s ease; 105 | background: $darkgrey; 106 | } 107 | } 108 | } 109 | } 110 | 111 | .side-menu-burg:focus { 112 | outline: none; 113 | } 114 | -------------------------------------------------------------------------------- /src/scss/infopopup.scss: -------------------------------------------------------------------------------- 1 | @import "burger"; 2 | 3 | .infopopup { 4 | width: $infopopup-width; 5 | box-shadow: 10px -10px 38px rgba(0, 0, 0, 0.3), 6 | 10px 15px 12px rgba(0, 0, 0, 0.22); 7 | color: $darkgrey; 8 | position: absolute; 9 | background: $offwhite-transparent; 10 | bottom: $timeline-height; 11 | left: $toolbar-width; 12 | border: 3px solid $offwhite; 13 | border-radius: 1px; 14 | padding: 20px; 15 | box-sizing: border-box; 16 | font-size: $large; 17 | transition: opacity 0.5s ease 0.1s, z-index 0.1s ease 0s; 18 | opacity: 1; 19 | z-index: $overheader; 20 | 21 | &.hidden { 22 | transition: 0.5s ease; 23 | opacity: 0; 24 | } 25 | 26 | .side-menu-burg { 27 | position: absolute; 28 | right: 8px; 29 | top: 10px; 30 | &.light { 31 | &.is-active span:after, 32 | &.is-active span:before { 33 | background: black; 34 | } 35 | } 36 | } 37 | 38 | &.dark { 39 | // background: $black-transparent; 40 | background: rgba(0, 0, 0, 0.8); 41 | color: white; 42 | } 43 | 44 | iframe { 45 | flex: 1; 46 | width: 100%; 47 | min-height: 400px; 48 | } 49 | 50 | @media (max-height: 1000px) { 51 | iframe { 52 | min-height: 230px; 53 | } 54 | } 55 | 56 | &.mobile { 57 | border: none; 58 | padding: 5vmin; 59 | .side-menu-burg { 60 | display: none; 61 | } 62 | } 63 | 64 | .legend { 65 | display: flex; 66 | flex-direction: column; 67 | } 68 | 69 | .legend-header { 70 | display: flex; 71 | flex-direction: row; 72 | justify-content: center; 73 | h2 { 74 | display: flex; 75 | font-size: 12pt; 76 | letter-spacing: 2px; 77 | margin: 0; 78 | } 79 | } 80 | 81 | .legend-container { 82 | height: 100%; 83 | display: flex; 84 | flex-direction: row; 85 | 86 | .legend-item { 87 | display: flex; 88 | justify-content: center; 89 | align-items: center; 90 | &.one { 91 | flex: 1; 92 | } 93 | &.three { 94 | flex: 5; 95 | } 96 | } 97 | } 98 | 99 | .legend-section { 100 | height: 25px; 101 | display: flex; 102 | align-items: center; 103 | 104 | svg { 105 | width: 60px; 106 | float: left; 107 | display: inline-block; 108 | } 109 | 110 | .legend-labels { 111 | display: flex; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/components/time/Categories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as d3 from "d3"; 3 | 4 | class TimelineCategories extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.grabRef = React.createRef(); 8 | this.state = { 9 | isInitialized: false, 10 | }; 11 | } 12 | 13 | componentDidUpdate() { 14 | if (!this.state.isInitialized) { 15 | const drag = d3 16 | .drag() 17 | .on("start", this.props.onDragStart) 18 | .on("drag", this.props.onDrag) 19 | .on("end", this.props.onDragEnd); 20 | 21 | d3.select(this.grabRef.current).call(drag); 22 | 23 | this.setState({ isInitialized: true }); 24 | } 25 | } 26 | 27 | renderCategory(cat, idx) { 28 | const { features, dims } = this.props; 29 | const strokeWidth = 1; // dims.trackHeight / (this.props.categories.length + 1) 30 | if ( 31 | features.GRAPH_NONLOCATED && 32 | features.GRAPH_NONLOCATED.categories && 33 | features.GRAPH_NONLOCATED.categories.includes(cat) 34 | ) { 35 | return null; 36 | } 37 | 38 | return ( 39 | <> 40 | 46 | 47 | 48 | 53 | 54 | {cat} 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | render() { 62 | const { dims, categories, fallbackLabel } = this.props; 63 | const categoriesExist = categories && categories.length > 0; 64 | const renderedCategories = categoriesExist 65 | ? categories.map((cat, idx) => this.renderCategory(cat, idx)) 66 | : this.renderCategory(fallbackLabel, 0); 67 | 68 | return ( 69 | 70 | {renderedCategories} 71 | 79 | 80 | ); 81 | } 82 | } 83 | 84 | export default TimelineCategories; 85 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right