);
}
@@ -268,7 +285,8 @@ class Toolbar extends React.Component {
features.USE_CATEGORIES,
numCategoryPanels || 0
);
- const shapesIdx = filtersIdx + 1;
+ const shapesIdx = filtersIdx + features.USE_SHAPES;
+ const downloadIdx = shapesIdx + features.USE_DOWNLOAD;
return (
@@ -310,6 +328,13 @@ class Toolbar extends React.Component {
panels.shapes.icon
)
: null}
+ {features.USE_DOWNLOAD
+ ? this.renderToolbarTab(
+ downloadIdx,
+ panels.download.label,
+ panels.download.icon
+ )
+ : null}
{features.USE_FULLSCREEN && (
)}
diff --git a/src/components/controls/BottomActions.js b/src/components/controls/BottomActions.js
index abb3241..b6f78b9 100644
--- a/src/components/controls/BottomActions.js
+++ b/src/components/controls/BottomActions.js
@@ -29,7 +29,13 @@ function BottomActions(props) {
) : null}
-
+
>
);
}
diff --git a/src/components/controls/DownloadButton.js b/src/components/controls/DownloadButton.js
new file mode 100644
index 0000000..4ac6a0d
--- /dev/null
+++ b/src/components/controls/DownloadButton.js
@@ -0,0 +1,86 @@
+import React from "react";
+import copy from "../../common/data/copy.json";
+import { parse } from "json2csv";
+import { downloadAsFile } from "../../common/utilities";
+
+export class DownloadButton extends React.Component {
+ onDownload(format, domain) {
+ let filename = `ukr-civharm-${this.datetimeToDateString(new Date())}`;
+ if (format === "csv") {
+ let outputData = this.getCsvData(domain);
+ downloadAsFile(`${filename}.csv`, outputData);
+ } else if (format === "json") {
+ let outputData = this.getJsonData(domain);
+ downloadAsFile(`${filename}.json`, outputData);
+ }
+ }
+ getCsvData(domain) {
+ const { events, sources } = domain;
+ const exportEvents = events.map((e) => {
+ return {
+ id: e.civId,
+ date: this.datetimeToDateString(e.datetime),
+ latitude: e.latitude,
+ longitude: e.longitude,
+ location: e.location,
+ description: e.description,
+ sources: e.sources.map((s) => sources[s].paths[0]).join(","),
+ associations: e.associations
+ .map((a) => a.filter_paths.join("="))
+ .join(","),
+ };
+ });
+ return parse(exportEvents, { flatten: true });
+ }
+ getJsonData(domain) {
+ const { events, sources } = domain;
+ const exportEvents = events.map((e) => {
+ return {
+ id: e.civId,
+ date: this.datetimeToDateString(e.datetime),
+ latitude: e.latitude,
+ longitude: e.longitude,
+ location: e.location,
+ description: e.description,
+ sources: e.sources.map((id) => {
+ const s = sources[id];
+ return {
+ id,
+ path: s.paths[0],
+ description: s.description,
+ };
+ }),
+ filters: e.associations.map((a) => {
+ return {
+ key: a.filter_paths[0],
+ value: a.filter_paths[1],
+ };
+ }),
+ };
+ });
+ return JSON.stringify(exportEvents);
+ }
+ datetimeToDateString(datetime) {
+ try {
+ return datetime.toISOString().split("T")[0];
+ } catch (_) {}
+ return "";
+ }
+ render() {
+ const { language, domain, format } = this.props;
+ const textByFormat = copy[language].toolbar.download.panel.formats[format];
+ return (
+
+ this.onDownload(format, domain)}
+ >
+ {"download"}
+ {textByFormat.label}
+
+ {textByFormat.description}
+
+ );
+ }
+}
diff --git a/src/components/controls/DownloadPanel.js b/src/components/controls/DownloadPanel.js
new file mode 100644
index 0000000..e68f3d4
--- /dev/null
+++ b/src/components/controls/DownloadPanel.js
@@ -0,0 +1,16 @@
+import React from "react";
+import { DownloadButton } from "./DownloadButton";
+
+const DownloadPanel = ({ language, title, description, domain }) => {
+ return (
+
+
{title}
+
{description}
+
+
+
+
+ );
+};
+
+export default DownloadPanel;
diff --git a/src/components/controls/atoms/Media.js b/src/components/controls/atoms/Media.js
index 55f694e..bfbf9a6 100644
--- a/src/components/controls/atoms/Media.js
+++ b/src/components/controls/atoms/Media.js
@@ -74,7 +74,10 @@ const Media = ({ src, title }) => {
return (
-
+
);
default:
diff --git a/src/components/space/carto/Map.js b/src/components/space/carto/Map.js
index 580249c..e688c93 100644
--- a/src/components/space/carto/Map.js
+++ b/src/components/space/carto/Map.js
@@ -2,6 +2,7 @@
import { bindActionCreators } from "redux";
import "leaflet";
import React from "react";
+import { flushSync } from "react-dom";
import { Portal } from "react-portal";
import Supercluster from "supercluster";
import { isMobileOnly } from "react-device-detect";
@@ -173,8 +174,10 @@ class Map extends React.Component {
this.map.dragging.enable();
this.map.doubleClickZoom.enable();
this.map.scrollWheelZoom.enable();
- this.alignLayers();
- this.updateClusters();
+ flushSync(() => {
+ this.alignLayers();
+ this.updateClusters();
+ });
});
map.on("zoomstart", () => {
if (this.svgRef.current !== null)
diff --git a/src/index.jsx b/src/index.jsx
index b983407..2fd4b07 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,7 +1,7 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
-import store from "./store";
+import store, { dispatchNavigatorLanguagesChange } from "./store";
import App from "./components/App";
import copy from "./common/data/copy.json";
@@ -19,6 +19,10 @@ root.render(
);
+// pick up user's preferred language on startup and whenever it changes
+dispatchNavigatorLanguagesChange();
+window.addEventListener("languagechange", dispatchNavigatorLanguagesChange);
+
store.subscribe(() => {
const { app } = store.getState();
renderAppLanguage(app);
diff --git a/src/reducers/app.js b/src/reducers/app.js
index c52f619..9b94879 100644
--- a/src/reducers/app.js
+++ b/src/reducers/app.js
@@ -1,6 +1,7 @@
import initial from "../store/initial.js";
import { ASSOCIATION_MODES } from "../common/constants";
import { toggleFlagAC } from "../common/utilities";
+import { pickPreferredLanguage } from "../common/language.js";
import * as selectors from "../selectors";
import {
@@ -31,6 +32,7 @@ import {
SET_INITIAL_CATEGORIES,
SET_INITIAL_SHAPES,
UPDATE_SEARCH_QUERY,
+ CHANGE_NAVIGATOR_LANGUAGES,
} from "../actions";
function updateHighlighted(appState, action) {
@@ -238,6 +240,13 @@ function toggleLanguage(appState, action) {
}
}
+function changeNavigatorLanguages(appState, action) {
+ const preferred = action.languages;
+ const available = appState.languages || [appState.language];
+ const language = pickPreferredLanguage(available, preferred);
+ return language ? { ...appState, language } : appState;
+}
+
function updateSource(appState, action) {
return {
...appState,
@@ -341,6 +350,8 @@ function app(appState = initial.app, action) {
return updateNarrativeStepIdx(appState, action);
case UPDATE_SOURCE:
return updateSource(appState, action);
+ case CHANGE_NAVIGATOR_LANGUAGES:
+ return changeNavigatorLanguages(appState, action);
/* toggles */
case TOGGLE_LANGUAGE:
return toggleLanguage(appState, action);
diff --git a/src/scss/cover.scss b/src/scss/cover.scss
index f00cf91..7670b93 100644
--- a/src/scss/cover.scss
+++ b/src/scss/cover.scss
@@ -1,343 +1,344 @@
.cover-container {
- position: absolute;
- top: -100%;
- left: 0;
- height: 100vh;
- background-color: black;
- width: 100%;
- opacity: 1;
- transition: top 0.4s ease;
- z-index: $loading-overlay + 1;
- overflow-y: auto;
- overflow-x: hidden;
- color: $offwhite;
+ position: absolute;
+ top: -100%;
+ left: 0;
+ height: 100vh;
+ background-color: black;
+ width: 100%;
+ opacity: 1;
+ transition: top 0.4s ease;
+ z-index: $loading-overlay + 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ color: $offwhite;
- &.showing {
- top: 0;
- left: 0;
- }
+ &.showing {
+ top: 0;
+ left: 0;
+ }
}
.cover-header {
- position: fixed;
- bottom: 20px;
- left: 0;
+ position: fixed;
+ bottom: 20px;
+ left: 0;
+ display: flex;
+ width: 100vw;
+
+ @media only screen and (max-width: 1200px) {
+ position: inherit;
+ }
+
+ .cover-logo-container {
+ padding: 20px 0 0 20px;
display: flex;
- width: 100vw;
-
- @media only screen and (max-width: 1200px) {
- position: inherit;
- }
-
- .cover-logo-container {
- padding: 20px 0 0 20px;
- display: flex;
-
- &.minimized {}
-
- .cover-logo {
- transition: all 1s;
- width: 60px;
- height: 60px;
- }
- }
&.minimized {
- bottom: 150px;
- max-width: $toolbar-width;
- max-height: 30px;
- justify-content: center;
- align-items: center;
- flex-direction: column;
-
- .cover-logo-container {
- padding: 5px;
- }
-
- .cover-logo {
- width: 60px;
- height: 60px;
- }
}
+
+ .cover-logo {
+ transition: all 1s;
+ width: 60px;
+ height: 60px;
+ }
+ }
+
+ &.minimized {
+ bottom: 150px;
+ max-width: $toolbar-width;
+ max-height: 30px;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+
+ .cover-logo-container {
+ padding: 5px;
+ }
+
+ .cover-logo {
+ width: 60px;
+ height: 60px;
+ }
+ }
}
.fullscreen-bg {
- &.hidden {
- top: -100%;
- }
+ &.hidden {
+ top: -100%;
+ }
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- // overflow: hidden;
- z-index: -100;
- background: #000000;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ // overflow: hidden;
+ z-index: -100;
+ background: #000000;
}
.fullscreen-bg__video {
- position: relative;
- top: 0;
- left: -25vw;
- width: 150vw;
- height: 100vh;
- -webkit-filter: contrast(70%) brightness(70%) grayscale(100%);
- filter: contrast(70%) brightness(70%) grayscale(100%);
+ position: relative;
+ top: 0;
+ left: -25vw;
+ width: 150vw;
+ height: 100vh;
+ -webkit-filter: contrast(70%) brightness(70%) grayscale(100%);
+ filter: contrast(70%) brightness(70%) grayscale(100%);
- @media only screen and (max-width: 992px) {
- display: none;
- }
+ @media only screen and (max-width: 992px) {
+ display: none;
+ }
}
.default-cover-container {
- display: flex;
- justify-content: center;
- flex-direction: column;
- align-items: center;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
}
.cover-container {
- font-size: 12pt;
+ font-size: 12pt;
+ display: flex;
+ flex-direction: column;
+ max-height: 100%;
+
+ hr,
+ br {
+ width: 100%;
+ }
+
+ .sidebar {
display: flex;
flex-direction: column;
- max-height: 100%;
-
- hr,
- br {
- width: 100%;
- }
-
- .sidebar {
- display: flex;
- flex-direction: column;
- justify-content: space-around;
- align-items: space-around;
- position: fixed;
- left: 0;
- background-color: $offwhite;
- margin-top: 60px;
- min-height: calc(100% - 280px);
- max-height: calc(100% - 280px);
- min-width: 19%;
- max-width: 19%;
- color: black;
-
- .il-video-pill {
- display: flex;
- justify-content: center;
- align-items: center;
- text-align: center;
- flex: 1;
- background-color: transparent;
- border-bottom: 5px solid black;
- transition: all 0.4s ease;
-
- &.explore {
- background-color: $yellow;
- }
-
- &.videos {
- background-color: blue;
- }
-
- &:hover {
- cursor: pointer;
- background-color: $darkwhite;
- color: white;
- }
- }
- }
-
- .hero {
- min-width: 100%;
- min-height: 80px;
- margin: auto;
- display: flex;
- flex-direction: column;
- margin-bottom: 20px;
- margin-top: 60px;
-
- @media only screen and (max-width: 1200px) {
- min-height: 250px;
- }
-
- .row {
- display: flex;
- flex: 1;
- flex-direction: row;
-
- @media only screen and (max-width: 1200px) {
- flex-direction: column;
- }
-
- justify-content: space-around;
-
- &.vertical {
- flex-direction: column;
- }
-
- .cell {
- border: 1px solid white;
- display: flex;
- justify-content: center;
- align-items: center;
- text-align: center;
- flex: 1;
- background-color: $darkgrey;
- padding: 10px 0;
- transition: all 0.4s ease;
- letter-spacing: 2px;
- min-height: 40px;
-
- &.small {
- letter-spacing: inherit;
- font-size: 10pt;
- }
-
- &.plain {
- min-height: 10px;
- background-color: black;
- letter-spacing: 1px;
-
- @media only screen and (max-width: 1200px) {
- min-height: 100px;
- }
- }
-
- &.yellow {
- color: black !important;
- background-color: $yellow;
- }
-
- &:hover {
- cursor: pointer;
- background-color: $darkwhite;
- color: white;
- }
-
- @media only screen and (max-width: 1200px) {
- min-height: 100px;
- }
- }
- }
- }
-
- .cover-content {
- display: flex;
- flex-direction: column;
- max-width: 600px;
- overflow-y: auto;
- overflow-x: hidden;
- padding-bottom: 10em;
-
- h1,
- h2,
- h3,
- h4,
- h5 {
- text-align: center;
- }
-
- h1 {
- margin-bottom: -15px;
- margin-top: 30px;
- }
-
- h5 {
- margin-top: -15px;
- }
-
- .md-container {
- width: 100%;
- overflow-wrap: break-word;
- // white-space: pre-line;
-
- ul {
- list-style: none;
- }
-
- li::before {
- content: "* ";
- }
-
- p {
- text-align: justify;
- }
- }
-
- // mobile styles, remove overlay buttons
- @media only screen and (max-width: 1200px) {
- font-size: 22pt !important;
- max-width: 100vw;
- padding: 0 40px 80px 40px;
- margin-bottom: 0;
- }
-
- .verify-tabs {
- background-color: $yellow;
- color: black;
- display: flex;
- flex-direction: column;
-
- .v-tab {
- display: flex;
- margin: auto;
- justify-content: center;
- align-content: center;
- flex: 1;
- }
- }
-
- .il-cover-verification-container {
- display: flex;
- flex-direction: column;
-
- .il-cover-verification {
- .il-video {
- border-radius: 1em;
- background-color: rgba(240, 240, 240, 0.5);
- }
- }
- }
- }
-
- _::-webkit-full-page-media,
- _:future,
- :root .cover-content {
- max-width: auto;
- }
-}
-
-.cover-footer {
- &.disabled {
- display: none;
- }
-
+ justify-content: space-around;
+ align-items: space-around;
position: fixed;
- bottom: 0;
- min-height: 150px;
- min-width: 100%;
- padding: 10px;
- background-color: black;
- display: flex;
- justify-content: center;
+ left: 0;
+ background-color: $offwhite;
+ margin-top: 60px;
+ min-height: calc(100% - 280px);
+ max-height: calc(100% - 280px);
+ min-width: 19%;
+ max-width: 19%;
+ color: black;
- .il-cover-button {
+ .il-video-pill {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ flex: 1;
+ background-color: transparent;
+ border-bottom: 5px solid black;
+ transition: all 0.4s ease;
+
+ &.explore {
+ background-color: $yellow;
+ }
+
+ &.videos {
+ background-color: blue;
+ }
+
+ &:hover {
+ cursor: pointer;
+ background-color: $darkwhite;
+ color: white;
+ }
+ }
+ }
+
+ .hero {
+ min-width: 100%;
+ min-height: 80px;
+ margin: auto;
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 20px;
+ margin-top: 60px;
+
+ @media only screen and (max-width: 1200px) {
+ min-height: 250px;
+ }
+
+ .row {
+ display: flex;
+ flex: 1;
+ flex-direction: row;
+
+ @media only screen and (max-width: 1200px) {
+ flex-direction: column;
+ }
+
+ justify-content: space-around;
+
+ &.vertical {
+ flex-direction: column;
+ }
+
+ .cell {
+ border: 1px solid white;
display: flex;
justify-content: center;
align-items: center;
- min-width: 300px;
- max-height: 80px;
- margin-top: 30px;
- background-color: $offwhite;
- color: black;
- transition: all 0.3s ease;
+ text-align: center;
+ flex: 1;
+ background-color: $darkgrey;
+ padding: 10px 0;
+ transition: all 0.4s ease;
+ letter-spacing: 2px;
+ min-height: 40px;
+
+ &.small {
+ letter-spacing: inherit;
+ font-size: 10pt;
+ }
+
+ &.plain {
+ min-height: 10px;
+ background-color: black;
+ letter-spacing: 1px;
+
+ @media only screen and (max-width: 1200px) {
+ min-height: 100px;
+ }
+ }
+
+ &.yellow {
+ color: black !important;
+ background-color: $yellow;
+ }
&:hover {
- cursor: pointer;
- background-color: darken($offwhite, 30%);
- color: black;
+ cursor: pointer;
+ background-color: $darkwhite;
+ color: white;
}
+
+ @media only screen and (max-width: 1200px) {
+ min-height: 100px;
+ }
+ }
}
-}
\ No newline at end of file
+ }
+
+ .cover-content {
+ display: flex;
+ flex-direction: column;
+ max-width: 600px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding-bottom: 10em;
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5 {
+ text-align: center;
+ }
+
+ h1 {
+ margin-bottom: -15px;
+ margin-top: 30px;
+ }
+
+ h5 {
+ margin-top: -15px;
+ }
+
+ .md-container {
+ width: 100%;
+ overflow-wrap: break-word;
+ // white-space: pre-line;
+
+ ul {
+ list-style: none;
+ }
+
+ li::before {
+ content: "* ";
+ }
+
+ p {
+ text-align: justify;
+ }
+ }
+
+ // mobile styles, remove overlay buttons
+ @media only screen and (max-width: 1200px) {
+ font-size: 22pt !important;
+ max-width: 100vw;
+ padding: 0 40px 80px 40px;
+ margin-bottom: 0;
+ }
+
+ .verify-tabs {
+ background-color: $yellow;
+ color: black;
+ display: flex;
+ flex-direction: column;
+
+ .v-tab {
+ display: flex;
+ margin: auto;
+ justify-content: center;
+ align-content: center;
+ flex: 1;
+ }
+ }
+
+ .il-cover-verification-container {
+ display: flex;
+ flex-direction: column;
+
+ .il-cover-verification {
+ .il-video {
+ border-radius: 1em;
+ background-color: rgba(240, 240, 240, 0.5);
+ }
+ }
+ }
+ }
+
+ _::-webkit-full-page-media,
+ _:future,
+ :root .cover-content {
+ max-width: auto;
+ }
+}
+
+.cover-footer {
+ &.disabled {
+ display: none;
+ }
+
+ position: fixed;
+ bottom: 0;
+ min-height: 150px;
+ min-width: 100%;
+ padding: 10px;
+ background-color: black;
+ display: flex;
+ justify-content: center;
+
+ .il-cover-button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-width: 300px;
+ max-height: 80px;
+ margin-top: 30px;
+ background-color: $offwhite;
+ color: black;
+ transition: all 0.3s ease;
+
+ &:hover {
+ cursor: pointer;
+ background-color: darken($offwhite, 30%);
+ color: black;
+ }
+ }
+}
diff --git a/src/scss/map.scss b/src/scss/map.scss
index 8ba39e6..42a10c2 100644
--- a/src/scss/map.scss
+++ b/src/scss/map.scss
@@ -117,6 +117,10 @@
&.hide {
display: none;
}
+
+ &:focus {
+ outline: none;
+ }
}
.leaflet-popup {
diff --git a/src/scss/toolbar.scss b/src/scss/toolbar.scss
index 912c8ba..79405e2 100644
--- a/src/scss/toolbar.scss
+++ b/src/scss/toolbar.scss
@@ -186,7 +186,21 @@
}
}
- .toolbar-tab {
+ .download-row {
+ display: flex;
+ flex-direction: row;
+ }
+ .download-button {
+ flex: 1 1 auto;
+ }
+ .download-description {
+ flex: 1 5 auto;
+ text-align: justify;
+ margin: auto;
+ }
+
+ .toolbar-tab,
+ .download-button {
display: flex;
align-items: center;
justify-content: center;
diff --git a/src/store/index.js b/src/store/index.js
index 19a5f6f..b88a42b 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -2,6 +2,8 @@ import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import rootReducer from "../reducers";
+import { changeNavigatorLanguages } from "../actions";
+
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
@@ -9,4 +11,9 @@ const store = createStore(
composeEnhancers(applyMiddleware(thunk))
);
+export function dispatchNavigatorLanguagesChange() {
+ const languages = navigator.languages || [navigator.language];
+ return store.dispatch(changeNavigatorLanguages(languages));
+}
+
export default store;
diff --git a/src/store/initial.js b/src/store/initial.js
index cc98318..d1f334d 100644
--- a/src/store/initial.js
+++ b/src/store/initial.js
@@ -140,6 +140,12 @@ const initial = {
title: copy[language].toolbar.explore_by_shape__title,
description: copy[language].toolbar.explore_by_shape__description,
},
+ download: {
+ icon: DEFAULT_TAB_ICONS.DOWNLOAD,
+ label: copy[language].toolbar.download.button,
+ title: copy[language].toolbar.download.panel.title,
+ description: copy[language].toolbar.download.panel.description,
+ },
},
},
loading: false,