diff --git a/config.js b/config.js index cee4c10..5f1e6a2 100644 --- a/config.js +++ b/config.js @@ -150,6 +150,7 @@ module.exports = { COLOR_BY_ASSOCIATION: true, USE_ASSOCIATIONS: true, USE_FULLSCREEN: true, + USE_DOWNLOAD: true, USE_SOURCES: true, USE_SPOTLIGHTS: false, USE_SHAPES: false, diff --git a/package-lock.json b/package-lock.json index ed221a1..49fa2bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "jest-resolve": "26.6.0", "jest-watch-typeahead": "0.6.1", "joi": "^14.0.1", + "json2csv": "^5.0.7", "leaflet": "^1.0.3", "lint-staged": "^10.5.3", "marked": "^0.7.0", @@ -83,7 +84,7 @@ "screenfull": "^5.2.0", "semver": "7.3.2", "style-loader": "1.3.0", - "supercluster": "^7.1.0", + "supercluster": "^7.1.5", "terser-webpack-plugin": "4.2.3", "ts-pnp": "1.2.0", "url-loader": "4.1.1", @@ -15261,6 +15262,31 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "devOptional": true }, + "node_modules/json2csv": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.7.tgz", + "integrity": "sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA==", + "dependencies": { + "commander": "^6.1.0", + "jsonparse": "^1.3.1", + "lodash.get": "^4.4.2" + }, + "bin": { + "json2csv": "bin/json2csv.js" + }, + "engines": { + "node": ">= 10", + "npm": ">= 6.13.0" + } + }, + "node_modules/json2csv/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/json3": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", @@ -15288,6 +15314,14 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "engines": [ + "node >= 0.2.0" + ] + }, "node_modules/jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -15845,6 +15879,11 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -23256,9 +23295,9 @@ } }, "node_modules/supercluster": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.4.tgz", - "integrity": "sha512-GhKkRM1jMR6WUwGPw05fs66pOFWhf59lXq+Q3J3SxPvhNcmgOtLRV6aVQPMRsmXdpaeFJGivt+t7QXUPL3ff4g==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", "dependencies": { "kdbush": "^3.0.0" } @@ -24434,19 +24473,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", - "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/ua-parser-js": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", @@ -37962,6 +37988,23 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "devOptional": true }, + "json2csv": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.7.tgz", + "integrity": "sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA==", + "requires": { + "commander": "^6.1.0", + "jsonparse": "^1.3.1", + "lodash.get": "^4.4.2" + }, + "dependencies": { + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + } + } + }, "json3": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", @@ -37981,6 +38024,11 @@ "universalify": "^2.0.0" } }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, "jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -38407,6 +38455,11 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -44233,9 +44286,9 @@ } }, "supercluster": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.4.tgz", - "integrity": "sha512-GhKkRM1jMR6WUwGPw05fs66pOFWhf59lXq+Q3J3SxPvhNcmgOtLRV6aVQPMRsmXdpaeFJGivt+t7QXUPL3ff4g==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", "requires": { "kdbush": "^3.0.0" } @@ -45128,12 +45181,6 @@ "is-typedarray": "^1.0.0" } }, - "typescript": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", - "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", - "peer": true - }, "ua-parser-js": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", diff --git a/package.json b/package.json index cf51440..1936304 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "jest-resolve": "26.6.0", "jest-watch-typeahead": "0.6.1", "joi": "^14.0.1", + "json2csv": "^5.0.7", "leaflet": "^1.0.3", "lint-staged": "^10.5.3", "marked": "^0.7.0", @@ -90,7 +91,7 @@ "screenfull": "^5.2.0", "semver": "7.3.2", "style-loader": "1.3.0", - "supercluster": "^7.1.0", + "supercluster": "^7.1.5", "terser-webpack-plugin": "4.2.3", "ts-pnp": "1.2.0", "url-loader": "4.1.1", diff --git a/src/actions/index.js b/src/actions/index.js index 266310c..a7cb06c 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -341,6 +341,14 @@ export function toggleLanguage(language) { }; } +export const CHANGE_NAVIGATOR_LANGUAGES = "CHANGE_NAVIGATOR_LANGUAGES"; +export function changeNavigatorLanguages(languages) { + return { + type: CHANGE_NAVIGATOR_LANGUAGES, + languages, + }; +} + export const CLOSE_TOOLBAR = "CLOSE_TOOLBAR"; export function closeToolbar() { return { diff --git a/src/common/constants.js b/src/common/constants.js index 0ad7ec5..58aeb54 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -11,6 +11,7 @@ export const DEFAULT_TAB_ICONS = { NARRATIVE: "timeline", FILTER: "filter_list", SHAPE: "change_history", + DOWNLOAD: "download", }; export const AVAILABLE_SHAPES = { @@ -24,8 +25,7 @@ export const AVAILABLE_SHAPES = { }; export const POLYGON_CLIP_PATH = { - STAR: - "polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)", + STAR: "polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)", DIAMOND: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)", PENTAGON: "polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)", TRIANGLE: "polygon(50% 0%, 0% 100%, 100% 100%)", diff --git a/src/common/data/copy.json b/src/common/data/copy.json index df30d74..e480786 100644 --- a/src/common/data/copy.json +++ b/src/common/data/copy.json @@ -160,7 +160,24 @@ "explore_by_shapes__title": "Explore events by shape breakdown", "explore_by_shape__description": "Shapes map to a given type of event that appears on the timeline.

Select the shape marker to toggle this type of event on / off", "fullscreen_enter": "Fullscreen", - "fullscreen_exit": "Exit Fullscreen" + "fullscreen_exit": "Exit Fullscreen", + "download": { + "button": "Download", + "panel": { + "title": "Download events", + "description": "Export the most recent available events in different formats.", + "formats": { + "csv": { + "label": "CSV", + "description": "CSV file where sources and filters are concatenated into a single column due to data structure limitations." + }, + "json": { + "label": "JSON", + "description": "JSON file where each event is a structured object containing nested arrays of sources and filters." + } + } + } + } }, "timeline": { "labels_title": "Testimonies", diff --git a/src/common/language.js b/src/common/language.js new file mode 100644 index 0000000..2a642db --- /dev/null +++ b/src/common/language.js @@ -0,0 +1,35 @@ +/** + * Picks the most preferred of the available languages. Comparison only on + * the primary language tag. Region, variant, and script tags are ignored. + * + * @param {readonly string[]} availableLanguages: Languages to pick from. + * @param {readonly string[]} preferredLanguages: Languages to prefer. + * @return {string|undefined} matching language, if any. + * @example + * pickPreferredLanguage(['en', 'ru', 'uk'], ['be-BY', 'ru-BY', 'en-UK']) // => 'ru' + */ +export function pickPreferredLanguage(availableLanguages, preferredLanguages) { + for (const preferredLanguage of preferredLanguages) { + const preferredLanguageCode = languageTagCode(preferredLanguage); + for (const availableLanguage of availableLanguages) { + const availableLanguageCode = languageTagCode(availableLanguage); + if (availableLanguageCode === preferredLanguageCode) + return availableLanguage; + } + } +} + +/** + * Takes the language tag as per RFC5646 ("uk", "uk-UA", "ru-BY", en-GB"). + * Returns the primary language-code subtag (lower-case, ISO 639). + * + * @param {string} languageTag language tag with one or more subtags + * @return {string|undefined} first subtag, two/three lowercase letters + * @example languageTagCode('en-US') // => 'en' + * @example languageTagCode('uk') // => 'uk' + * @see https://tools.ietf.org/html/rfc5646 + */ +export function languageTagCode(languageTag) { + const matches = languageTag.toLowerCase().match(/^[a-z]{2,3}/); + return (matches ?? [])[0]; +} diff --git a/src/common/language.spec.js b/src/common/language.spec.js new file mode 100644 index 0000000..50caa90 --- /dev/null +++ b/src/common/language.spec.js @@ -0,0 +1,31 @@ +import { languageTagCode, pickPreferredLanguage } from "./language.js"; + +describe("language tag matching", () => { + test("languageTagCode", () => { + expect(languageTagCode("en-US")).toEqual("en"); + expect(languageTagCode("uk")).toEqual("uk"); + expect(languageTagCode("UK")).toEqual("uk"); + expect(languageTagCode("i-")).toBeUndefined(); + expect(languageTagCode("-")).toBeUndefined(); + expect(languageTagCode("")).toBeUndefined(); + }); + test("pickPreferredLanguage", () => { + expect(pickPreferredLanguage(["en", "ru", "uk"], ["en-GB"])).toBe("en"); + expect(pickPreferredLanguage(["en-US", "uk-UA"], ["en-GB"])).toBe("en-US"); + expect( + pickPreferredLanguage( + ["en", "ru", "uk"], + ["pl-PL", "uk-UA", "ru-RU", "en-GB"] + ) + ).toBe("uk"); + expect( + pickPreferredLanguage( + ["en-US", "ru-RU", "uk-UA"], + ["pl-PL", "ru-BY", "en-GB"] + ) + ).toBe("ru-RU"); + expect( + pickPreferredLanguage(["en", "uk"], ["sv-SE", "fr-FR"]) + ).toBeUndefined(); + }); +}); diff --git a/src/common/utilities.js b/src/common/utilities.js index c768660..126e3e9 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -576,4 +576,20 @@ export function getFilterIdx( else return 0; } +export function downloadAsFile(filename, content) { + let element = document.createElement("a"); + element.setAttribute( + "href", + `data:application/octet-stream;charset=utf-8,${encodeURIComponent(content)}` + ); + element.setAttribute("download", filename); + + element.style.display = "none"; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +} + export const isEmptyString = (s) => s.length === 0; diff --git a/src/components/Layout.js b/src/components/Layout.js index 0f3e358..2221e16 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -307,6 +307,7 @@ class Dashboard extends React.Component { {checkMobile ? null : ( diff --git a/src/components/Toolbar.js b/src/components/Toolbar.js index e60b3cd..9377713 100644 --- a/src/components/Toolbar.js +++ b/src/components/Toolbar.js @@ -24,6 +24,7 @@ import { import { ToolbarButton } from "./controls/atoms/ToolbarButton"; import { FullscreenToggle } from "./controls/FullScreenToggle"; import { LanguageSwitch } from "./controls/LanguageSwitch"; +import DownloadPanel from "./controls/DownloadPanel"; class Toolbar extends React.Component { constructor(props) { @@ -181,6 +182,21 @@ class Toolbar extends React.Component { } } + renderToolbarDownloadPanel() { + const { panels } = this.props.toolbarCopy; + + return ( + + + + ); + } + renderToolbarTab(_selected, label, iconKey, key) { return ( ); } @@ -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}
-
Made with TimeMap
Free software from
Forensic Architecture
+
+ Made with{" "} + TimeMap +
+ Free software from
{" "} + Forensic Architecture +
); } 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,