mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 21:08:36 +03:00
Ingesting config through Create React App
This commit is contained in:
@@ -1,69 +1,69 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import * as selectors from '../selectors'
|
||||
import { getFilterIdxFromColorSet } from '../common/utilities'
|
||||
import * as selectors from "../selectors";
|
||||
import { getFilterIdxFromColorSet } from "../common/utilities";
|
||||
// import Card from './Card.jsx'
|
||||
import { Card } from '@forensic-architecture/design-system/react'
|
||||
import copy from '../common/data/copy.json'
|
||||
import { Card } from "@forensic-architecture/design-system/react";
|
||||
import copy from "../common/data/copy.json";
|
||||
|
||||
class CardStack extends React.Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.refs = {}
|
||||
this.refCardStack = React.createRef()
|
||||
this.refCardStackContent = React.createRef()
|
||||
constructor() {
|
||||
super();
|
||||
this.refs = {};
|
||||
this.refCardStack = React.createRef();
|
||||
this.refCardStackContent = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const isNarrative = !!this.props.narrative
|
||||
componentDidUpdate() {
|
||||
const isNarrative = !!this.props.narrative;
|
||||
|
||||
if (isNarrative) {
|
||||
this.scrollToCard()
|
||||
this.scrollToCard();
|
||||
}
|
||||
}
|
||||
|
||||
scrollToCard () {
|
||||
const duration = 500
|
||||
const element = this.refCardStack.current
|
||||
scrollToCard() {
|
||||
const duration = 500;
|
||||
const element = this.refCardStack.current;
|
||||
const cardScroll = this.refs[this.props.narrative.current].current
|
||||
.offsetTop
|
||||
.offsetTop;
|
||||
|
||||
let start = element.scrollTop
|
||||
let change = cardScroll - start
|
||||
let currentTime = 0
|
||||
const increment = 20
|
||||
let start = element.scrollTop;
|
||||
let change = cardScroll - start;
|
||||
let currentTime = 0;
|
||||
const increment = 20;
|
||||
|
||||
// t = current time
|
||||
// b = start value
|
||||
// c = change in value
|
||||
// d = duration
|
||||
Math.easeInOutQuad = function (t, b, c, d) {
|
||||
t /= d / 2
|
||||
if (t < 1) return (c / 2) * t * t + b
|
||||
t -= 1
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b
|
||||
}
|
||||
t /= d / 2;
|
||||
if (t < 1) return (c / 2) * t * t + b;
|
||||
t -= 1;
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b;
|
||||
};
|
||||
|
||||
const animateScroll = function () {
|
||||
currentTime += increment
|
||||
const val = Math.easeInOutQuad(currentTime, start, change, duration)
|
||||
element.scrollTop = val
|
||||
if (currentTime < duration) setTimeout(animateScroll, increment)
|
||||
}
|
||||
animateScroll()
|
||||
currentTime += increment;
|
||||
const val = Math.easeInOutQuad(currentTime, start, change, duration);
|
||||
element.scrollTop = val;
|
||||
if (currentTime < duration) setTimeout(animateScroll, increment);
|
||||
};
|
||||
animateScroll();
|
||||
}
|
||||
|
||||
renderCards (events, selections) {
|
||||
renderCards(events, selections) {
|
||||
// if no selections provided, select all
|
||||
if (!selections) {
|
||||
selections = events.map((e) => true)
|
||||
selections = events.map((e) => true);
|
||||
}
|
||||
this.refs = []
|
||||
this.refs = [];
|
||||
|
||||
return events.map((event, idx) => {
|
||||
const thisRef = React.createRef()
|
||||
this.refs[idx] = thisRef
|
||||
const thisRef = React.createRef();
|
||||
this.refs[idx] = thisRef;
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -72,109 +72,109 @@ class CardStack extends React.Component {
|
||||
event,
|
||||
colors: this.props.colors,
|
||||
coloringSet: this.props.coloringSet,
|
||||
getFilterIdxFromColorSet
|
||||
getFilterIdxFromColorSet,
|
||||
})}
|
||||
language={this.props.language}
|
||||
isLoading={this.props.isLoading}
|
||||
isSelected={selections[idx]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderSelectedCards () {
|
||||
const { selected } = this.props
|
||||
renderSelectedCards() {
|
||||
const { selected } = this.props;
|
||||
|
||||
if (selected.length > 0) {
|
||||
return this.renderCards(selected)
|
||||
return this.renderCards(selected);
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
renderNarrativeCards () {
|
||||
const { narrative } = this.props
|
||||
const showing = narrative.steps
|
||||
renderNarrativeCards() {
|
||||
const { narrative } = this.props;
|
||||
const showing = narrative.steps;
|
||||
|
||||
const selections = showing.map((_, idx) => idx === narrative.current)
|
||||
const selections = showing.map((_, idx) => idx === narrative.current);
|
||||
|
||||
return this.renderCards(showing, selections)
|
||||
return this.renderCards(showing, selections);
|
||||
}
|
||||
|
||||
renderCardStackHeader () {
|
||||
const headerLang = copy[this.props.language].cardstack.header
|
||||
renderCardStackHeader() {
|
||||
const headerLang = copy[this.props.language].cardstack.header;
|
||||
|
||||
return (
|
||||
<div
|
||||
id='card-stack-header'
|
||||
className='card-stack-header'
|
||||
id="card-stack-header"
|
||||
className="card-stack-header"
|
||||
onClick={() => this.props.onToggleCardstack()}
|
||||
>
|
||||
<button className='side-menu-burg is-active'>
|
||||
<button className="side-menu-burg is-active">
|
||||
<span />
|
||||
</button>
|
||||
<p className='header-copy top'>
|
||||
<p className="header-copy top">
|
||||
{`${this.props.selected.length} ${headerLang}`}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderCardStackContent () {
|
||||
renderCardStackContent() {
|
||||
return (
|
||||
<div id='card-stack-content' className='card-stack-content'>
|
||||
<div id="card-stack-content" className="card-stack-content">
|
||||
<ul>{this.renderSelectedCards()}</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderNarrativeContent () {
|
||||
renderNarrativeContent() {
|
||||
return (
|
||||
<div
|
||||
id='card-stack-content'
|
||||
className='card-stack-content'
|
||||
id="card-stack-content"
|
||||
className="card-stack-content"
|
||||
ref={this.refCardStackContent}
|
||||
>
|
||||
<ul>{this.renderNarrativeCards()}</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isCardstack, selected, narrative, timelineDims } = this.props
|
||||
render() {
|
||||
const { isCardstack, selected, narrative, timelineDims } = this.props;
|
||||
// TODO: make '237px', which is the narrative header, less hard-coded
|
||||
const height = `calc(100% - 237px - ${timelineDims.height}px)`
|
||||
const height = `calc(100% - 237px - ${timelineDims.height}px)`;
|
||||
if (selected.length > 0) {
|
||||
if (!narrative) {
|
||||
return (
|
||||
<div
|
||||
id='card-stack'
|
||||
id="card-stack"
|
||||
className={`card-stack
|
||||
${isCardstack ? '' : ' folded'}`}
|
||||
${isCardstack ? "" : " folded"}`}
|
||||
>
|
||||
{this.renderCardStackHeader()}
|
||||
{this.renderCardStackContent()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
id='card-stack'
|
||||
id="card-stack"
|
||||
ref={this.refCardStack}
|
||||
className={`card-stack narrative-mode
|
||||
${isCardstack ? '' : ' folded'}`}
|
||||
${isCardstack ? "" : " folded"}`}
|
||||
style={{ height }}
|
||||
>
|
||||
{this.renderNarrativeContent()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
narrative: selectors.selectActiveNarrative(state),
|
||||
selected: selectors.selectSelected(state),
|
||||
@@ -185,8 +185,8 @@ function mapStateToProps (state) {
|
||||
cardUI: state.ui.card,
|
||||
colors: state.ui.coloring.colors,
|
||||
coloringSet: state.app.associations.coloringSet,
|
||||
features: state.features
|
||||
}
|
||||
features: state.features,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CardStack)
|
||||
export default connect(mapStateToProps)(CardStack);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
import colors from '../../../common/global.js'
|
||||
import ColoredMarkers from './ColoredMarkers.jsx'
|
||||
import React, { useState } from "react";
|
||||
import { Portal } from "react-portal";
|
||||
import colors from "../../../common/global.js";
|
||||
import ColoredMarkers from "./ColoredMarkers.jsx";
|
||||
import {
|
||||
calcClusterOpacity,
|
||||
calcClusterSize,
|
||||
@@ -9,18 +9,30 @@ import {
|
||||
isLongitude,
|
||||
calculateColorPercentages,
|
||||
zipColorsToPercentages,
|
||||
calculateTotalClusterPoints } from '../../../common/utilities'
|
||||
calculateTotalClusterPoints,
|
||||
} from "../../../common/utilities";
|
||||
|
||||
const DefsClusters = () => (
|
||||
<defs>
|
||||
<radialGradient id='clusterGradient'>
|
||||
<stop offset='10%' stop-color='red' />
|
||||
<stop offset='90%' stop-color='transparent' />
|
||||
<radialGradient id="clusterGradient">
|
||||
<stop offset="10%" stop-color="red" />
|
||||
<stop offset="90%" stop-color="transparent" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
)
|
||||
);
|
||||
|
||||
function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHover, onClick, getClusterChildren, coloringSet, filterColors }) {
|
||||
function Cluster({
|
||||
cluster,
|
||||
size,
|
||||
projectPoint,
|
||||
totalPoints,
|
||||
styles,
|
||||
renderHover,
|
||||
onClick,
|
||||
getClusterChildren,
|
||||
coloringSet,
|
||||
filterColors,
|
||||
}) {
|
||||
/**
|
||||
{
|
||||
geometry: {
|
||||
@@ -35,22 +47,25 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
|
||||
type: "Feature"
|
||||
}
|
||||
*/
|
||||
const { cluster_id: clusterId } = cluster.properties
|
||||
const { cluster_id: clusterId } = cluster.properties;
|
||||
|
||||
const individualChildren = getClusterChildren(clusterId)
|
||||
const colorPercentages = calculateColorPercentages(individualChildren, coloringSet)
|
||||
const individualChildren = getClusterChildren(clusterId);
|
||||
const colorPercentages = calculateColorPercentages(
|
||||
individualChildren,
|
||||
coloringSet
|
||||
);
|
||||
|
||||
const { coordinates } = cluster.geometry
|
||||
const [longitude, latitude] = coordinates
|
||||
if (!isLatitude(latitude) || !isLongitude(longitude)) return null
|
||||
const { x, y } = projectPoint([latitude, longitude])
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const { coordinates } = cluster.geometry;
|
||||
const [longitude, latitude] = coordinates;
|
||||
const { x, y } = projectPoint([latitude, longitude]);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
if (!isLatitude(latitude) || !isLongitude(longitude)) return null;
|
||||
|
||||
return (
|
||||
<g
|
||||
className={'cluster-event'}
|
||||
className={"cluster-event"}
|
||||
transform={`translate(${x}, ${y})`}
|
||||
onClick={e => onClick({ id: clusterId, latitude, longitude })}
|
||||
onClick={(e) => onClick({ id: clusterId, latitude, longitude })}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
@@ -58,16 +73,16 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
|
||||
radius={size}
|
||||
colorPercentMap={zipColorsToPercentages(filterColors, colorPercentages)}
|
||||
styles={{
|
||||
...styles
|
||||
...styles,
|
||||
}}
|
||||
className={'cluster-event-marker'}
|
||||
className={"cluster-event-marker"}
|
||||
/>
|
||||
{hovered ? renderHover(cluster) : null}
|
||||
</g>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ClusterEvents ({
|
||||
function ClusterEvents({
|
||||
projectPoint,
|
||||
onSelect,
|
||||
getClusterChildren,
|
||||
@@ -76,56 +91,66 @@ function ClusterEvents ({
|
||||
svg,
|
||||
clusters,
|
||||
filterColors,
|
||||
selected
|
||||
selected,
|
||||
}) {
|
||||
const totalPoints = calculateTotalClusterPoints(clusters)
|
||||
const totalPoints = calculateTotalClusterPoints(clusters);
|
||||
|
||||
const styles = {
|
||||
fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor,
|
||||
stroke: colors.darkBackground,
|
||||
strokeWidth: 0
|
||||
}
|
||||
strokeWidth: 0,
|
||||
};
|
||||
|
||||
function renderHover (txt, circleSize) {
|
||||
return <>
|
||||
<text text-anchor='middle' y='3px' style={{ fontWeight: 'bold', fill: 'black', zIndex: 10000 }}>{txt}</text>
|
||||
<circle
|
||||
class='event-hover'
|
||||
cx='0'
|
||||
cy='0'
|
||||
r={circleSize + 2}
|
||||
stroke={colors.primaryHighlight}
|
||||
fill-opacity='0.0'
|
||||
/>
|
||||
</>
|
||||
function renderHover(txt, circleSize) {
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
text-anchor="middle"
|
||||
y="3px"
|
||||
style={{ fontWeight: "bold", fill: "black", zIndex: 10000 }}
|
||||
>
|
||||
{txt}
|
||||
</text>
|
||||
<circle
|
||||
class="event-hover"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r={circleSize + 2}
|
||||
stroke={colors.primaryHighlight}
|
||||
fill-opacity="0.0"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal node={svg}>
|
||||
<g className='cluster-locations'>
|
||||
<g className="cluster-locations">
|
||||
{isRadial ? <DefsClusters /> : null}
|
||||
{clusters.map(c => {
|
||||
const pointCount = c.properties.point_count
|
||||
const clusterSize = calcClusterSize(pointCount, totalPoints)
|
||||
return <Cluster
|
||||
onClick={onSelect}
|
||||
getClusterChildren={getClusterChildren}
|
||||
coloringSet={coloringSet}
|
||||
cluster={c}
|
||||
filterColors={filterColors}
|
||||
size={clusterSize}
|
||||
projectPoint={projectPoint}
|
||||
totalPoints={totalPoints}
|
||||
styles={{
|
||||
...styles,
|
||||
fillOpacity: calcClusterOpacity(pointCount, totalPoints)
|
||||
}}
|
||||
renderHover={() => renderHover(pointCount, clusterSize)}
|
||||
/>
|
||||
{clusters.map((c) => {
|
||||
const pointCount = c.properties.point_count;
|
||||
const clusterSize = calcClusterSize(pointCount, totalPoints);
|
||||
return (
|
||||
<Cluster
|
||||
onClick={onSelect}
|
||||
getClusterChildren={getClusterChildren}
|
||||
coloringSet={coloringSet}
|
||||
cluster={c}
|
||||
filterColors={filterColors}
|
||||
size={clusterSize}
|
||||
projectPoint={projectPoint}
|
||||
totalPoints={totalPoints}
|
||||
styles={{
|
||||
...styles,
|
||||
fillOpacity: calcClusterOpacity(pointCount, totalPoints),
|
||||
}}
|
||||
renderHover={() => renderHover(pointCount, clusterSize)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterEvents
|
||||
export default ClusterEvents;
|
||||
|
||||
Reference in New Issue
Block a user