mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 13:28:36 +03:00
house Toolbar components in folder
This commit is contained in:
37
src/components/Toolbar/BottomActions.js
Normal file
37
src/components/Toolbar/BottomActions.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
|
||||
import SitesIcon from '../presentational/Icons/Sites'
|
||||
import CoverIcon from '../presentational/Icons/Cover'
|
||||
import InfoIcon from '../presentational/Icons/Info'
|
||||
|
||||
function BottomActions (props) {
|
||||
function renderToggles () {
|
||||
return [
|
||||
<div className='bottom-action-block'>
|
||||
{process.env.features.USE_SITES ? <SitesIcon
|
||||
isActive={props.sites.enabled}
|
||||
onClickHandler={props.sites.toggle}
|
||||
/> : null}
|
||||
</div>,
|
||||
<div className='botttom-action-block'>
|
||||
<InfoIcon
|
||||
isActive={props.info.enabled}
|
||||
onClickHandler={props.info.toggle}
|
||||
/>
|
||||
</div>,
|
||||
<div className='botttom-action-block'>
|
||||
{process.env.features.USE_COVER ? <CoverIcon
|
||||
onClickHandler={props.cover.toggle}
|
||||
/> : null}
|
||||
</div>
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bottom-actions'>
|
||||
{renderToggles()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BottomActions
|
||||
38
src/components/Toolbar/CategoriesListPanel.js
Normal file
38
src/components/Toolbar/CategoriesListPanel.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import Checkbox from '../presentational/Checkbox'
|
||||
import copy from '../../js/data/copy.json'
|
||||
|
||||
export default ({
|
||||
categories,
|
||||
activeCategories,
|
||||
onCategoryFilter,
|
||||
language
|
||||
}) => {
|
||||
function renderCategoryTree () {
|
||||
return (
|
||||
<div>
|
||||
{categories.map(cat => {
|
||||
return (<li
|
||||
key={cat.category.replace(/ /g, '_')}
|
||||
className={'tag-filter active'}
|
||||
style={{ marginLeft: '20px' }}
|
||||
>
|
||||
<Checkbox
|
||||
label={cat.category}
|
||||
isActive={activeCategories.includes(cat.category)}
|
||||
onClickCheckbox={() => onCategoryFilter(cat.category)}
|
||||
/>
|
||||
</li>)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='react-innertabpanel'>
|
||||
<h2>{copy[language].toolbar.categories}</h2>
|
||||
<p>{copy[language].toolbar.explore_by_category__description}</p>
|
||||
{renderCategoryTree()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
218
src/components/Toolbar/Layout.js
Normal file
218
src/components/Toolbar/Layout.js
Normal file
@@ -0,0 +1,218 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import * as actions from '../../actions'
|
||||
import * as selectors from '../../selectors'
|
||||
|
||||
import { Tabs, TabPanel } from 'react-tabs'
|
||||
import Search from './Search'
|
||||
import TagListPanel from './TagListPanel'
|
||||
import CategoriesListPanel from './CategoriesListPanel'
|
||||
import BottomActions from './BottomActions'
|
||||
import copy from '../../js/data/copy.json'
|
||||
import { trimAndEllipse } from '../../js/utilities.js'
|
||||
|
||||
class Toolbar extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = { _selected: -1 }
|
||||
}
|
||||
|
||||
selectTab (selected) {
|
||||
const _selected = (this.state._selected === selected) ? -1 : selected
|
||||
this.setState({ _selected })
|
||||
}
|
||||
|
||||
renderClosePanel () {
|
||||
return (
|
||||
<div className='panel-header' onClick={() => this.selectTab(-1)}>
|
||||
<div className='caret' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderSearch () {
|
||||
if (process.env.features.USE_SEARCH) {
|
||||
return (
|
||||
<TabPanel>
|
||||
<Search
|
||||
language={this.props.language}
|
||||
tags={this.props.tags}
|
||||
categories={this.props.categories}
|
||||
tagFilters={this.props.tagFilters}
|
||||
categoryFilters={this.props.categoryFilters}
|
||||
filter={this.props.filter}
|
||||
/>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
goToNarrative (narrative) {
|
||||
this.selectTab(-1) // set all unselected within this component
|
||||
this.props.methods.onSelectNarrative(narrative)
|
||||
}
|
||||
|
||||
renderToolbarNarrativePanel () {
|
||||
return (
|
||||
<TabPanel>
|
||||
<h2>{copy[this.props.language].toolbar.narrative_panel_title}</h2>
|
||||
<p>{copy[this.props.language].toolbar.narrative_summary}</p>
|
||||
{this.props.narratives.map((narr) => {
|
||||
return (
|
||||
<div className='panel-action action'>
|
||||
<button style={{ backgroundColor: '#000' }} onClick={() => { this.goToNarrative(narr) }}>
|
||||
<p>{narr.label}</p>
|
||||
<p><small>{trimAndEllipse(narr.description, 120)}</small></p>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
|
||||
renderToolbarCategoriesPanel () {
|
||||
if (process.env.features.CATEGORIES_AS_TAGS) {
|
||||
return (
|
||||
<TabPanel>
|
||||
<CategoriesListPanel
|
||||
categories={this.props.categories}
|
||||
activeCategories={this.props.activeCategories}
|
||||
onCategoryFilter={this.props.methods.onCategoryFilter}
|
||||
language={this.props.language}
|
||||
/>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
renderToolbarTagPanel () {
|
||||
if (process.env.features.USE_TAGS &&
|
||||
this.props.tags.children) {
|
||||
return (
|
||||
<TabPanel>
|
||||
<TagListPanel
|
||||
tags={this.props.tags}
|
||||
activeTags={this.props.activeTags}
|
||||
onTagFilter={this.props.methods.onTagFilter}
|
||||
language={this.props.language}
|
||||
/>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
renderToolbarTab (_selected, label, iconKey) {
|
||||
const isActive = (this.state._selected === _selected)
|
||||
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'
|
||||
|
||||
return (
|
||||
<div className={classes} onClick={() => { this.selectTab(_selected) }}>
|
||||
<i className='material-icons'>{iconKey}</i>
|
||||
<div className='tab-caption'>{label}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderToolbarPanels () {
|
||||
let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded'
|
||||
return (
|
||||
<div className={classes}>
|
||||
{this.renderClosePanel()}
|
||||
<Tabs selectedIndex={this.state._selected}>
|
||||
{this.renderToolbarNarrativePanel()}
|
||||
{this.renderToolbarCategoriesPanel()}
|
||||
{this.renderToolbarTagPanel()}
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderToolbarNavs () {
|
||||
if (this.props.narratives) {
|
||||
return this.props.narratives.map((nar, idx) => {
|
||||
const isActive = (idx === this.state._selected)
|
||||
|
||||
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'
|
||||
|
||||
return (
|
||||
<div className={classes} onClick={() => { this.selectTab(idx) }}>
|
||||
<div className='tab-caption'>{nar.label}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
renderToolbarTabs () {
|
||||
let title = copy[this.props.language].toolbar.title
|
||||
if (process.env.title) title = process.env.title
|
||||
const narrativesLabel = copy[this.props.language].toolbar.narratives_label
|
||||
const tagsLabel = copy[this.props.language].toolbar.tags_label
|
||||
const categoriesLabel = 'Categories' // TODO:
|
||||
const isTags = this.props.tags && this.props.tags.children
|
||||
const isCategories = true
|
||||
|
||||
return (
|
||||
<div className='toolbar'>
|
||||
<div className='toolbar-header'><p>{title}</p></div>
|
||||
<div className='toolbar-tabs'>
|
||||
{this.renderToolbarTab(0, narrativesLabel, 'timeline')}
|
||||
{(isCategories) ? this.renderToolbarTab(1, categoriesLabel, 'widgets') : null}
|
||||
{(isTags) ? this.renderToolbarTab(2, tagsLabel, 'filter_list') : null}
|
||||
</div>
|
||||
<BottomActions
|
||||
info={{
|
||||
enabled: this.props.infoShowing,
|
||||
toggle: this.props.actions.toggleInfoPopup
|
||||
}}
|
||||
sites={{
|
||||
enabled: this.props.sitesShowing,
|
||||
toggle: this.props.actions.toggleSites
|
||||
}}
|
||||
cover={{
|
||||
toggle: this.props.actions.toggleCover
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isNarrative } = this.props
|
||||
|
||||
return (
|
||||
<div id='toolbar-wrapper' className={`toolbar-wrapper ${(isNarrative) ? 'narrative-mode' : ''}`}>
|
||||
{this.renderToolbarTabs()}
|
||||
{this.renderToolbarPanels()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
tags: selectors.getTagTree(state),
|
||||
categories: selectors.getCategories(state),
|
||||
narratives: selectors.selectNarratives(state),
|
||||
language: state.app.language,
|
||||
activeTags: selectors.getActiveTags(state),
|
||||
activeCategories: selectors.getActiveCategories(state),
|
||||
viewFilters: state.app.filters.views,
|
||||
features: state.app.features,
|
||||
narrative: state.app.narrative,
|
||||
sitesShowing: state.app.flags.isShowingSites,
|
||||
infoShowing: state.app.flags.isInfopopup
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Toolbar)
|
||||
72
src/components/Toolbar/Search.js
Normal file
72
src/components/Toolbar/Search.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* global fetch */
|
||||
import React from 'react'
|
||||
import copy from '../../js/data/copy.json'
|
||||
import TagFilter from './TagFilter'
|
||||
|
||||
export default class Search extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
searchValue: undefined,
|
||||
searchResults: []
|
||||
}
|
||||
|
||||
this.handleSearchChange = this.handleSearchChange.bind(this)
|
||||
this.handleSearchSubmit = this.handleSearchSubmit.bind(this)
|
||||
}
|
||||
|
||||
handleSearchSubmit (e) {
|
||||
e.preventDefault()
|
||||
fetch(`api/search/${this.state.searchValue}`)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
this.setState({
|
||||
searchResults: json.tags
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleSearchChange (event) {
|
||||
this.setState({ searchValue: event.target.value })
|
||||
}
|
||||
|
||||
renderSearchResults () {
|
||||
return (
|
||||
this.state.searchResults.map(tag => {
|
||||
return (
|
||||
<TagFilter
|
||||
isShowTree
|
||||
tags={this.props.tags}
|
||||
categories={this.props.categories}
|
||||
tagFilters={this.props.tagFilters}
|
||||
categoryFilters={this.props.categoryFilters}
|
||||
filter={this.props.filter}
|
||||
tag={tag}
|
||||
isCategory={this.props.isCategory}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='search-content'>
|
||||
<h2>{copy[this.props.language].toolbar.panels.search.title}</h2>
|
||||
<form onSubmit={this.handleSearchSubmit}>
|
||||
<input
|
||||
value={this.state.searchValue}
|
||||
onChange={this.handleSearchChange}
|
||||
autoFocus
|
||||
type='text'
|
||||
name='search-input'
|
||||
placeholder={copy[this.props.language].toolbar.panels.search.placeholder}
|
||||
/>
|
||||
</form>
|
||||
<ul>
|
||||
{this.renderSearchResults()}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
82
src/components/Toolbar/TagFilter.js
Normal file
82
src/components/Toolbar/TagFilter.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react'
|
||||
import Checkbox from '../presentational/Checkbox'
|
||||
|
||||
function TagFilter (props) {
|
||||
function isActive () {
|
||||
if (props.isCategory) {
|
||||
return props.categoryFilters.includes(props.tag.id)
|
||||
}
|
||||
return props.tagFilters.includes(props.tag.id)
|
||||
}
|
||||
|
||||
function onClickTag () {
|
||||
if (isActive()) {
|
||||
props.filter({
|
||||
tags: props.tagFilters.filter(element => element !== props.tag.id)
|
||||
})
|
||||
} else {
|
||||
props.filter({
|
||||
tags: props.tagFilters.concat(props.tag.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onClickCategory () {
|
||||
if (isActive()) {
|
||||
props.filter({
|
||||
categories: props.categoryFilters.filter(element => element !== props.tag.id)
|
||||
})
|
||||
} else {
|
||||
props.filter({
|
||||
categories: props.categoryFilters.concat(props.tag.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function renderTag () {
|
||||
const tag = props.tag
|
||||
let classes = (isActive()) ? 'tag-filter active' : 'tag-filter'
|
||||
let label = `${tag.name} ( ${tag.mentions} )`
|
||||
if (props.isShowTree) {
|
||||
label = `${tag.group} > ${tag.subgroup} > ${tag.name} ( ${tag.mentions} )`
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={props.tag.id}
|
||||
className={classes}
|
||||
>
|
||||
<Checkbox
|
||||
isActive={isActive()}
|
||||
label={label}
|
||||
onClickCheckbox={() => onClickTag()}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function renderCategory () {
|
||||
const category = props.categories[props.tag.id]
|
||||
let classes = (isActive()) ? 'tag-filter active' : 'tag-filter'
|
||||
|
||||
if (category) {
|
||||
return (
|
||||
<li
|
||||
key={props.tag.id}
|
||||
className={classes}
|
||||
>
|
||||
<Checkbox
|
||||
isActive={isActive()}
|
||||
label={`${category.name} ( ${category.counts} )`}
|
||||
onClickCheckbox={onClickCategory}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return (<div />)
|
||||
}
|
||||
|
||||
if (props.isCategory) return (renderCategory())
|
||||
return (renderTag())
|
||||
}
|
||||
|
||||
export default TagFilter
|
||||
45
src/components/Toolbar/TagListPanel.js
Normal file
45
src/components/Toolbar/TagListPanel.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react'
|
||||
import Checkbox from '../presentational/Checkbox'
|
||||
import copy from '../../js/data/copy.json'
|
||||
|
||||
function TagListPanel ({
|
||||
tags,
|
||||
activeTags,
|
||||
onTagFilter,
|
||||
language
|
||||
}) {
|
||||
function createNodeComponent (node, depth) {
|
||||
return (
|
||||
<li
|
||||
key={node.key.replace(/ /g, '_')}
|
||||
className={'tag-filter active'}
|
||||
style={{ marginLeft: `${depth * 20}px` }}
|
||||
>
|
||||
<Checkbox
|
||||
label={node.key}
|
||||
isActive={activeTags.includes(node.key)}
|
||||
onClickCheckbox={() => onTagFilter(node.key)}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function renderTree () {
|
||||
/* NOTE: only render first layer of tags */
|
||||
return (
|
||||
<div>
|
||||
{Object.values(tags.children).map(tag => createNodeComponent(tag, 1))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='react-innertabpanel'>
|
||||
<h2>{copy[language].toolbar.tags}</h2>
|
||||
<p>{copy[language].toolbar.explore_by_tag__description}</p>
|
||||
{renderTree()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagListPanel
|
||||
Reference in New Issue
Block a user