mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 13:28:36 +03:00
significantly refactor presentational components
This commit is contained in:
BIN
src/components/.DS_Store
vendored
Normal file
BIN
src/components/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -7,14 +7,14 @@ import {
|
||||
import React from 'react'
|
||||
|
||||
import Spinner from './presentational/Spinner'
|
||||
import CardTimestamp from './presentational/CardTimestamp'
|
||||
import CardLocation from './presentational/CardLocation'
|
||||
import CardCaret from './presentational/CardCaret'
|
||||
import CardTags from './presentational/CardTags'
|
||||
import CardSummary from './presentational/CardSummary'
|
||||
import CardSource from './presentational/CardSource'
|
||||
import CardCategory from './presentational/CardCategory'
|
||||
import CardNarrative from './presentational/CardNarrative'
|
||||
import CardTimestamp from './presentational/Card/Timestamp'
|
||||
import CardLocation from './presentational/Card/Location'
|
||||
import CardCaret from './presentational/Card/Caret'
|
||||
import CardTags from './presentational/Card/Tags'
|
||||
import CardSummary from './presentational/Card/Summary'
|
||||
import CardSource from './presentational/Card/Source'
|
||||
import CardCategory from './presentational/Card/Category'
|
||||
import CardNarrative from './presentational/Card/Narrative'
|
||||
|
||||
class Card extends React.Component {
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import LoadingOverlay from './presentational/LoadingOverlay'
|
||||
import Map from './Map.jsx'
|
||||
import Toolbar from './Toolbar.jsx'
|
||||
import CardStack from './CardStack.jsx'
|
||||
import NarrativeControls from './presentational/NarrativeControls.js'
|
||||
import NarrativeControls from './presentational/Narrative/Controls.js'
|
||||
import InfoPopUp from './InfoPopup.jsx'
|
||||
import Timeline from './Timeline.jsx'
|
||||
import Notification from './Notification.jsx'
|
||||
|
||||
@@ -9,17 +9,18 @@ import 'leaflet';
|
||||
|
||||
import { isNotNullNorUndefined } from '../js/utilities';
|
||||
|
||||
import MapSites from './MapSites.jsx';
|
||||
import MapShapes from './MapShapes.jsx';
|
||||
import MapEvents from './MapEvents.jsx';
|
||||
import MapSelectedEvents from './MapSelectedEvents.jsx';
|
||||
import MapNarratives from './MapNarratives.jsx';
|
||||
import MapDefsMarkers from './MapDefsMarkers.jsx';
|
||||
import MapSites from './presentational/Map/Sites.jsx';
|
||||
import MapShapes from './presentational/Map/Shapes.jsx';
|
||||
import MapEvents from './presentational/Map/Events.jsx';
|
||||
import MapSelectedEvents from './presentational/Map/SelectedEvents.jsx';
|
||||
import MapNarratives from './presentational/Map/Narratives.jsx';
|
||||
import MapDefsMarkers from './presentational/Map/DefsMarkers.jsx';
|
||||
|
||||
class Map extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.projectPoint = this.projectPoint.bind(this)
|
||||
this.svgRef = React.createRef();
|
||||
this.map = null;
|
||||
this.state = {
|
||||
@@ -109,6 +110,14 @@ class Map extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1])
|
||||
return {
|
||||
x: this.map.latLngToLayerPoint(latLng).x + this.state.mapTransformX,
|
||||
y: this.map.latLngToLayerPoint(latLng).y + this.state.mapTransformY
|
||||
}
|
||||
}
|
||||
|
||||
getClientDims() {
|
||||
const boundingClient = document.querySelector(`#${this.props.ui.dom.map}`).getBoundingClientRect();
|
||||
|
||||
@@ -140,9 +149,7 @@ class Map extends React.Component {
|
||||
return (
|
||||
<MapSites
|
||||
sites={this.props.domain.sites}
|
||||
map={this.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
projectPoint={this.projectPoint}
|
||||
isEnabled={this.props.app.views.sites}
|
||||
/>
|
||||
);
|
||||
@@ -153,9 +160,7 @@ class Map extends React.Component {
|
||||
<MapShapes
|
||||
svg={this.svgRef.current}
|
||||
shapes={this.props.domain.shapes}
|
||||
map={this.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
projectPoint={this.projectPoint}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -165,9 +170,7 @@ class Map extends React.Component {
|
||||
<MapNarratives
|
||||
svg={this.svgRef.current}
|
||||
narratives={this.props.domain.narratives}
|
||||
map={this.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
projectPoint={this.projectPoint}
|
||||
narrative={this.props.app.narrative}
|
||||
narrativeProps={this.props.ui.narratives}
|
||||
onSelect={this.props.methods.onSelect}
|
||||
@@ -200,9 +203,7 @@ class Map extends React.Component {
|
||||
locations={this.props.domain.locations}
|
||||
styleLocation={this.styleLocation}
|
||||
categories={this.props.domain.categories}
|
||||
map={this.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
projectPoint={this.projectPoint}
|
||||
narrative={this.props.app.narrative}
|
||||
onSelect={this.props.methods.onSelect}
|
||||
onSelectNarrative={this.props.methods.onSelectNarrative}
|
||||
@@ -216,9 +217,7 @@ class Map extends React.Component {
|
||||
<MapSelectedEvents
|
||||
svg={this.svgRef.current}
|
||||
selected={this.props.app.selected}
|
||||
map={this.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
projectPoint={this.projectPoint}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import hash from 'object-hash';
|
||||
|
||||
import copy from '../js/data/copy.json';
|
||||
import { formatterWithYear, parseDate } from '../js/utilities';
|
||||
import TimelineHeader from './presentational/TimelineHeader';
|
||||
import TimelineHeader from './presentational/Timeline/Header';
|
||||
import TimelineAxis from './TimelineAxis.jsx';
|
||||
import TimelineClip from './presentational/TimelineClip';
|
||||
import TimelineHandles from './presentational/TimelineHandles.js';
|
||||
import TimelineZoomControls from './presentational/TimelineZoomControls.js';
|
||||
import TimelineLabels from './presentational/TimelineLabels.js';
|
||||
import TimelineMarkers from './presentational/TimelineMarkers.js'
|
||||
import TimelineEvents from './presentational/TimelineEvents.js';
|
||||
import TimelineClip from './presentational/Timeline/Clip';
|
||||
import TimelineHandles from './presentational/Timeline/Handles.js';
|
||||
import TimelineZoomControls from './presentational/Timeline/ZoomControls.js';
|
||||
import TimelineLabels from './presentational/Timeline/Labels.js';
|
||||
import TimelineMarkers from './presentational/Timeline/Markers.js'
|
||||
import TimelineEvents from './presentational/Timeline/Events.js';
|
||||
import TimelineCategories from './TimelineCategories.jsx';
|
||||
|
||||
class Timeline extends React.Component {
|
||||
|
||||
BIN
src/components/presentational/.DS_Store
vendored
Normal file
BIN
src/components/presentational/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { capitalizeFirstLetter } from '../../js/utilities.js';
|
||||
import { capitalizeFirstLetter } from '../../../js/utilities.js';
|
||||
|
||||
const CardCategory = ({ categoryTitle, categoryLabel, color }) => (
|
||||
<div className="card-row card-cell category">
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import copy from '../../js/data/copy.json';
|
||||
import { isNotNullNorUndefined } from '../../js/utilities';
|
||||
import copy from '../../../js/data/copy.json';
|
||||
import { isNotNullNorUndefined } from '../../../js/utilities';
|
||||
|
||||
const CardLocation = ({ language, location }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import CardNarrativeLink from './CardNarrativeLink';
|
||||
import CardNarrativeLink from './NarrativeLink';
|
||||
|
||||
const CardNarrative = (props) => (
|
||||
<div className="card-row">
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Spinner from './Spinner'
|
||||
import Img from 'react-image'
|
||||
|
||||
import copy from '../../js/data/copy.json'
|
||||
import Spinner from '../Spinner'
|
||||
import copy from '../../../js/data/copy.json'
|
||||
|
||||
const CardSource = ({ source, isLoading, onClickHandler }) => {
|
||||
function renderIconText(type) {
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import copy from '../../js/data/copy.json';
|
||||
import copy from '../../../js/data/copy.json';
|
||||
|
||||
const CardSummary = ({ language, description, isHighlighted }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import copy from '../../js/data/copy.json';
|
||||
import copy from '../../../js/data/copy.json';
|
||||
|
||||
const CardTags = ({ tags, language }) => {
|
||||
const tags_lang = copy[language].cardstack.tags;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import copy from '../../js/data/copy.json';
|
||||
import { isNotNullNorUndefined } from '../../js/utilities';
|
||||
import copy from '../../../js/data/copy.json';
|
||||
import { isNotNullNorUndefined } from '../../../js/utilities';
|
||||
|
||||
const CardTimestamp = ({ makeTimelabel, language, timestamp }) => {
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Portal } from 'react-portal';
|
||||
|
||||
class MapEvents extends React.Component {
|
||||
|
||||
projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return {
|
||||
x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX,
|
||||
y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY
|
||||
};
|
||||
}
|
||||
|
||||
getLocationEventsDistribution(location) {
|
||||
function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation, narrative, onSelect, svg, locations }){
|
||||
function getLocationEventsDistribution(location) {
|
||||
const eventCount = {};
|
||||
const categories = this.props.categories;
|
||||
const categories = categories;
|
||||
|
||||
categories.forEach(cat => {
|
||||
eventCount[cat.category] = [];
|
||||
@@ -26,7 +17,7 @@ class MapEvents extends React.Component {
|
||||
return eventCount;
|
||||
}
|
||||
|
||||
renderLocation(location) {
|
||||
function renderLocation(location) {
|
||||
/**
|
||||
{
|
||||
events: [...],
|
||||
@@ -35,23 +26,23 @@ class MapEvents extends React.Component {
|
||||
longitude: '32.2'
|
||||
}
|
||||
*/
|
||||
const { x, y } = this.projectPoint([location.latitude, location.longitude]);
|
||||
// const eventsByCategory = this.getLocationEventsDistribution(location);
|
||||
const { x, y } = projectPoint([location.latitude, location.longitude]);
|
||||
// const eventsByCategory = getLocationEventsDistribution(location);
|
||||
|
||||
const locCategory = location.events.length > 0 ? location.events[0].category : 'default'
|
||||
const customStyles = this.props.styleLocation ? this.props.styleLocation(location) : null
|
||||
const customStyles = styleLocation ? styleLocation(location) : null
|
||||
const extraStyles = customStyles[0]
|
||||
const extraRender = customStyles[1]
|
||||
|
||||
const styles = ({
|
||||
fill: this.props.getCategoryColor(locCategory),
|
||||
fill: getCategoryColor(locCategory),
|
||||
fillOpacity: 1,
|
||||
...customStyles[0]
|
||||
})
|
||||
|
||||
// in narrative mode, only render events in narrative
|
||||
if (this.props.narrative) {
|
||||
const { steps } = this.props.narrative
|
||||
if (narrative) {
|
||||
const { steps } = narrative
|
||||
const onlyIfInNarrative = e => steps.map(s => s.id).includes(e.id)
|
||||
const eventsInNarrative = location.events.filter(onlyIfInNarrative)
|
||||
|
||||
@@ -64,7 +55,7 @@ class MapEvents extends React.Component {
|
||||
<g
|
||||
className="location"
|
||||
transform={`translate(${x}, ${y})`}
|
||||
onClick={() => this.props.onSelect(location.events)}
|
||||
onClick={() => onSelect(location.events)}
|
||||
>
|
||||
<circle
|
||||
className="location-event-marker"
|
||||
@@ -77,13 +68,11 @@ class MapEvents extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Portal node={this.props.svg}>
|
||||
{this.props.locations.map(loc => this.renderLocation(loc))}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Portal node={svg}>
|
||||
{locations.map(renderLocation)}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapEvents;
|
||||
@@ -1,76 +1,67 @@
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
|
||||
class MapNarratives extends React.Component {
|
||||
|
||||
projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1])
|
||||
return {
|
||||
x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX,
|
||||
y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY
|
||||
}
|
||||
}
|
||||
|
||||
getNarrativeStyle(narrativeId) {
|
||||
const styleName = (narrativeId && narrativeId in this.props.narrativeProps)
|
||||
function MapNarratives ({ narrativeProps, onSelectNarrative, svg, narrative, narratives, projectPoint }) {
|
||||
function getNarrativeStyle(narrativeId) {
|
||||
const styleName = (narrativeId && narrativeId in narrativeProps)
|
||||
? narrativeId
|
||||
: 'default'
|
||||
return this.props.narrativeProps[styleName]
|
||||
return narrativeProps[styleName]
|
||||
}
|
||||
|
||||
getStepStyle(name) {
|
||||
function getStepStyle(name) {
|
||||
if (name === 'None') return null
|
||||
return this.props.narrativeProps.stepStyles[name]
|
||||
return narrativeProps.stepStyles[name]
|
||||
}
|
||||
|
||||
hasNoLocation(step) {
|
||||
function hasNoLocation(step) {
|
||||
return (step.latitude === '' || step.longitude === '')
|
||||
}
|
||||
|
||||
renderNarrativeStep(idx, n) {
|
||||
function renderNarrativeStep(idx, n) {
|
||||
const step = n.steps[idx]
|
||||
const step2 = n.steps[idx + 1]
|
||||
|
||||
// don't draw if one of the steps has no location
|
||||
if (this.hasNoLocation(step) || this.hasNoLocation(step2))
|
||||
if (hasNoLocation(step) || hasNoLocation(step2))
|
||||
return null
|
||||
|
||||
// 0 if not in narrative mode, 1 if active narrative, 0.1 if inactive
|
||||
let styles = {
|
||||
strokeOpacity: (n === null) ? 0
|
||||
: (step && (n.id === this.props.narrative.id)) ? 1 : 0.1,
|
||||
: (step && (n.id === narrative.id)) ? 1 : 0.1,
|
||||
strokeWidth: 0,
|
||||
strokeDasharray: 'none',
|
||||
stroke: 'none'
|
||||
}
|
||||
|
||||
const p1 = this.projectPoint([step.latitude, step.longitude])
|
||||
const p2 = this.projectPoint([step2.latitude, step2.longitude])
|
||||
const p1 = projectPoint([step.latitude, step.longitude])
|
||||
const p2 = projectPoint([step2.latitude, step2.longitude])
|
||||
|
||||
if (step) {
|
||||
if (process.env.features.NARRATIVE_STEP_STYLES) {
|
||||
const _idx = step.narratives.indexOf(n.id)
|
||||
const stepStyle = step.narrative___stepStyles[_idx]
|
||||
|
||||
return this._renderNarrativeStep(
|
||||
return _renderNarrativeStep(
|
||||
p1,
|
||||
p2,
|
||||
{ ...styles, ...this.getStepStyle(stepStyle) }
|
||||
{ ...styles, ...getStepStyle(stepStyle) }
|
||||
)
|
||||
|
||||
// otherwise steps are styled per narrative
|
||||
} else {
|
||||
styles = {
|
||||
...styles,
|
||||
...this.getNarrativeStyle(n.id)
|
||||
...getNarrativeStyle(n.id)
|
||||
}
|
||||
return this._renderNarrativeStep(p1,p2,styles)
|
||||
return _renderNarrativeStep(p1,p2,styles)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_renderNarrativeStep(p1, p2, styles) {
|
||||
function _renderNarrativeStep(p1, p2, styles) {
|
||||
const { stroke, strokeWidth, strokeDasharray, strokeOpacity } = styles
|
||||
return (
|
||||
<line
|
||||
@@ -80,7 +71,7 @@ class MapNarratives extends React.Component {
|
||||
y1={p1.y}
|
||||
y2={p2.y}
|
||||
markerStart="none"
|
||||
onClick={() => this.props.onSelectNarrative(n)}
|
||||
onClick={() => onSelectNarrative(n)}
|
||||
style={{
|
||||
strokeWidth,
|
||||
strokeDasharray,
|
||||
@@ -93,26 +84,23 @@ class MapNarratives extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
renderNarrative(n) {
|
||||
function renderNarrative(n) {
|
||||
const steps = n.steps.slice(0, n.steps.length - 1)
|
||||
|
||||
console.log(n)
|
||||
return (
|
||||
<g id={`narrative-${n.id.replace(/ /g,"_")}`} className="narrative">
|
||||
{steps.map((s, idx) => this.renderNarrativeStep(idx, n))}
|
||||
{steps.map((s, idx) => renderNarrativeStep(idx, n))}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.narrative === null) return (<div />)
|
||||
if (narrative === null) return (<div />)
|
||||
|
||||
return (
|
||||
<Portal node={this.props.svg}>
|
||||
{this.props.narratives.map(n => this.renderNarrative(n))}
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Portal node={svg}>
|
||||
{narratives.map(n => renderNarrative(n))}
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export default MapNarratives
|
||||
@@ -2,17 +2,8 @@ import React from 'react';
|
||||
import { Portal } from 'react-portal';
|
||||
|
||||
class MapSelectedEvents extends React.Component {
|
||||
|
||||
projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return {
|
||||
x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX,
|
||||
y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY
|
||||
};
|
||||
}
|
||||
|
||||
renderMarker (event) {
|
||||
const { x, y } = this.projectPoint([event.latitude, event.longitude]);
|
||||
const { x, y } = this.props.projectPoint([event.latitude, event.longitude]);
|
||||
return (
|
||||
<g
|
||||
className="location-marker"
|
||||
@@ -42,4 +33,4 @@ class MapSelectedEvents extends React.Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
export default MapSelectedEvents;
|
||||
export default MapSelectedEvents;
|
||||
@@ -1,15 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
|
||||
function MapShapes({ svg, map, shapes, mapTransformX, mapTransformY }) {
|
||||
function projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1])
|
||||
return {
|
||||
x: map.latLngToLayerPoint(latLng).x + mapTransformX,
|
||||
y: map.latLngToLayerPoint(latLng).y + mapTransformY
|
||||
}
|
||||
}
|
||||
|
||||
function MapShapes({ svg, shapes, projectPoint }) {
|
||||
function renderShape(shape, lineStyle) {
|
||||
const lineCoords = []
|
||||
const points = shape.points
|
||||
@@ -1,14 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
function MapSites({ map, sites, mapTransformX, mapTransformY }) {
|
||||
function projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return {
|
||||
x: map.latLngToLayerPoint(latLng).x + mapTransformX,
|
||||
y: map.latLngToLayerPoint(latLng).y + mapTransformY
|
||||
};
|
||||
}
|
||||
|
||||
function MapSites({ sites, projectPoint }) {
|
||||
function renderSite(site) {
|
||||
const { x, y } = projectPoint([site.latitude, site.longitude]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { selectActiveNarrative } from '../../selectors'
|
||||
import { selectActiveNarrative } from '../../../selectors'
|
||||
|
||||
function NarrativeCard ({ narrative }) {
|
||||
// no display if no narrative
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import NarrativeCard from './NarrativeCard'
|
||||
import NarrativeAdjust from './NarrativeAdjust'
|
||||
import NarrativeClose from './NarrativeClose'
|
||||
import Card from './Card'
|
||||
import Adjust from './Adjust'
|
||||
import Close from './Close'
|
||||
|
||||
export default ({ narrative, methods }) => {
|
||||
if (!narrative) return null
|
||||
@@ -12,18 +12,18 @@ export default ({ narrative, methods }) => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NarrativeCard narrative={narrative} />
|
||||
<NarrativeAdjust
|
||||
<Card narrative={narrative} />
|
||||
<Adjust
|
||||
isDisabled={!prevExists}
|
||||
direction='left'
|
||||
onClickHandler={methods.onPrev}
|
||||
/>
|
||||
<NarrativeAdjust
|
||||
<Adjust
|
||||
isDisabled={!nextExists}
|
||||
direction='right'
|
||||
onClickHandler={methods.onNext}
|
||||
/>
|
||||
<NarrativeClose
|
||||
<Close
|
||||
onClickHandler={() => methods.onSelectNarrative(null)}
|
||||
closeMsg='-- exit from narrative --'
|
||||
/>
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { formatterWithYear } from '../../js/utilities.js';
|
||||
import { formatterWithYear } from '../../../js/utilities.js';
|
||||
|
||||
const TimelineLabels = ({ dims, timelabels }) => {
|
||||
|
||||
Reference in New Issue
Block a user