diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..20440c1 --- /dev/null +++ b/src/App.js @@ -0,0 +1,105 @@ +import React, { Component } from 'react'; +import queryString from 'query-string'; +import axios from 'axios'; +import './styles/App.css'; +import TopMenuBar from './components/TopMenuBar'; +import Map from './Map'; + +class App extends Component { + constructor(props) { + super(props); + const query = queryString.parse(window.location.search); + this.state = { + searchTerm: null, + mappingData: null, + loading: false, + inputURL: query.url ? query.url : '', + }; + } + + componentDidMount() { + if (this.state.inputURL) { + this.handleInputURL(this.state.inputURL); + } + } + + handleInputURL = (url) => { + const testURL = url.replace('end=0', 'end=1').concat('&format=json'); + let query = null; + this.setState({ loading: true }); + axios + .get(testURL) + .then((response) => response.data) + .then((data) => { + query = data.query; + query.end = data.results_length; + this.fetchData(query, testURL); + }) + .catch((error) => { + console.error(error); + this.setState({ loading: false, searchTerm: null }); + }); + }; + + fetchData = (query, testURL) => { + const i = testURL.indexOf('/query'); + const baseURL = testURL.substring(0, i + 6); + axios + .get(baseURL, { params: query }) + .then((response) => response.data) + .then((data) => { + //console.log(data); + this.setState({ searchTerm: query.q }); + this.processData(data); + }) + .catch((error) => { + console.error(error); + }); + }; + + processData = ({ results }) => { + const dioceseMap = {}; + const mappingData = {}; + mappingData.searchTerm = this.state.searchTerm; + results.forEach((result) => { + const { context, metadata_fields: metadata } = result; + const { record_id, diocese_id } = metadata; + if (diocese_id) { + if (dioceseMap.hasOwnProperty(diocese_id)) { + if (!dioceseMap[diocese_id].has(record_id)) { + dioceseMap[diocese_id].add(record_id); + mappingData[diocese_id].push({ metadata, context }); + } + } else { + dioceseMap[diocese_id] = new Set([record_id]); + mappingData[diocese_id] = [{ metadata, context }]; + } + } + }); + //console.log(mappingData); + this.setState({ + mappingData, + loading: false, + }); + }; + + render() { + return ( +
+ + + {this.state.loading && ( +
+
+
+ )} +
+ ); + } +} + +export default App; diff --git a/src/Map.js b/src/Map.js new file mode 100644 index 0000000..8733385 --- /dev/null +++ b/src/Map.js @@ -0,0 +1,104 @@ +import React, { Component } from 'react'; +import { Map as LeafletMap, ScaleControl } from 'react-leaflet'; +import mapConfig from './assets/map_config'; +import './styles/Map.css'; +import MapBoxLayer from './components/MapBoxLayer'; +import ControlPanel from './components/ControlPanel'; +import InfoPanel from './components/InfoPanel'; +import ColorLegend from './components/ColorLegend'; +import RecordsModal from './components/RecordsModal'; +import GeoJSONLayer from './components/GeoJSONLayer'; + +class LocalLegislationMap extends Component { + constructor(props) { + super(props); + this.state = { + config: mapConfig, + currentColorScheme: 'color1', + info: null, + recordsModalOpen: false, + searchResults: null, + }; + this.mapRef = React.createRef(); + } + + showRecordsModal = (searchResults) => { + this.setState({ + searchResults: searchResults, + recordsModalOpen: true, + }); + }; + + closeRecordsModal = () => { + this.setState({ recordsModalOpen: false }); + }; + + changeColorScheme = (colorScheme) => { + this.setState({ + currentColorScheme: colorScheme, + }); + }; + + updateInfo = (info) => { + this.setState({ + info: info, + }); + }; + + getMaxNumEntries = () => { + const { mappingData } = this.props; + let maxNumEntries = 0; + for (const prop in mappingData) { + if (mappingData.hasOwnProperty(prop)) { + const numEntries = mappingData[prop].length; + if (numEntries > maxNumEntries) { + maxNumEntries = numEntries; + } + } + } + return maxNumEntries; + }; + + render() { + const config = this.state.config; + const maxNumEntries = this.getMaxNumEntries(); + return ( +
+ + + + + + + + + +
+ ); + } +} + +export default LocalLegislationMap; diff --git a/src/assets/map_config.js b/src/assets/map_config.js index c03223d..5e0004e 100644 --- a/src/assets/map_config.js +++ b/src/assets/map_config.js @@ -5,7 +5,7 @@ config.params = { zoomControl: false, zoom: 5, maxZoom: 9, - minZoom: 5, + minZoom: 4, scrollwheel: false, legends: true, infoControl: false, diff --git a/src/components/App.js b/src/components/App.js deleted file mode 100644 index 9697fdc..0000000 --- a/src/components/App.js +++ /dev/null @@ -1,234 +0,0 @@ -import React, { Component } from 'react'; -import './App.css'; -import Map from './Map'; -import queryString from 'query-string'; -import axios from 'axios'; -import { Button, Menu, Modal, Header, Icon, Form } from 'semantic-ui-react'; - -//============== -// Top Menu Bar -//============== - -class TopMenuBar extends Component { - render() { - return ( - - - React Logo - - - - - - - {this.props.searchTerm && ( - - Current query: "{this.props.searchTerm}" - - )} - - - v 0.4.1 - - - ); - } -} - -//========================== -// Modal to paste query url -//========================== - -class ModalQuery extends React.Component { - constructor(props) { - super(props); - this.state = { - modalOpen: false, - url: this.props.inputURL, - }; - } - - handleOpen = () => this.setState({ modalOpen: true }); - - handleClose = () => this.setState({ modalOpen: false }); - - handleChange = (event) => this.setState({ url: event.target.value }); - - handleSubmit = (event) => { - this.props.handleInputURL(this.state.url); - this.setState({ modalOpen: false }); - this.setState({ url: '' }); - }; - - render() { - return ( - - Map Search Results - - } - open={this.state.modalOpen} - onClose={this.handleClose} - size="small" - > -
- - -
- - - - -
-
- - - - - ); - } -} - -const ModalDescription = () => ( -
- To map your search results, -
    -
  • - Go to the PhiloLogic database:{' '} - - /corpus - {' '} - or{' '} - - /corpusnorm - -
  • -
  • Make a search.
  • -
  • - Click "Map All Results" button on the top-right corner. (recommended) -
  • -
  • Alternatively, you can paste in the search URL below.
  • -
-
-); - -//==================== -// Main App Component -//==================== - -class App extends Component { - constructor(props) { - super(props); - const query = queryString.parse(window.location.search); - this.state = { - searchTerm: null, - mappingData: null, - loading: false, - inputURL: query.url ? query.url : '', - }; - } - - componentDidMount() { - if (this.state.inputURL) { - this.handleInputURL(this.state.inputURL); - } - } - - handleInputURL = (url) => { - const testURL = url.replace('end=0', 'end=1').concat('&format=json'); - let query = null; - this.setState({ loading: true }); - axios - .get(testURL) - .then((response) => response.data) - .then((data) => { - query = data.query; - query.end = data.results_length; - this.fetchData(query, testURL); - }) - .catch((error) => { - console.error(error); - this.setState({ loading: false, searchTerm: null }); - }); - }; - - fetchData = (query, testURL) => { - const i = testURL.indexOf('/query'); - const baseURL = testURL.substring(0, i + 6); - axios - .get(baseURL, { params: query }) - .then((response) => response.data) - .then((data) => { - //console.log(data); - this.setState({ searchTerm: query.q }); - this.processData(data); - }) - .catch((error) => { - console.error(error); - }); - }; - - processData = ({ results }) => { - const dioceseMap = {}; - const mappingData = {}; - mappingData.searchTerm = this.state.searchTerm; - results.forEach((result) => { - const { context, metadata_fields: metadata } = result; - const { record_id, diocese_id } = metadata; - if (diocese_id) { - if (dioceseMap.hasOwnProperty(diocese_id)) { - if (!dioceseMap[diocese_id].has(record_id)) { - dioceseMap[diocese_id].add(record_id); - mappingData[diocese_id].push({ metadata, context }); - } - } else { - dioceseMap[diocese_id] = new Set([record_id]); - mappingData[diocese_id] = [{ metadata, context }]; - } - } - }); - //console.log(mappingData); - this.setState({ - mappingData, - loading: false, - }); - }; - - render() { - return ( -
- - - {this.state.loading && ( -
-
-
- )} -
- ); - } -} - -export default App; diff --git a/src/components/ColorLegend.js b/src/components/ColorLegend.js new file mode 100644 index 0000000..005cf43 --- /dev/null +++ b/src/components/ColorLegend.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +import { Card } from 'semantic-ui-react'; + +class ColorLegend extends Component { + render() { + if (!this.props.mappingData) { + return null; + } + const { maxNumEntries, currentColorScheme } = this.props; + const { colorSchemes } = this.props.config; + const colors = colorSchemes[currentColorScheme]; + const numPerBucket = Math.ceil(maxNumEntries / colors.length); + + const getStyle = (colorHex) => ({ + background: colorHex, + }); + + const colorBlocks = []; + + colorBlocks.push( + + + 0 +
+
+ ); + + for (let i = 0; i < colors.length; i++) { + const start = i * numPerBucket + 1; + const end = (i + 1) * numPerBucket; + let range = `${start} - ${end}`; + if (i === colors.length - 1) { + range = `> ${start}`; + } + colorBlocks.push( + + + {range} +
+
+ ); + } + + return ( + + {colorBlocks} + + ); + } +} + +export default ColorLegend; diff --git a/src/components/ControlPanel.js b/src/components/ControlPanel.js new file mode 100644 index 0000000..11427ea --- /dev/null +++ b/src/components/ControlPanel.js @@ -0,0 +1,47 @@ +import React, { Component } from 'react'; +import { Card, Dropdown, Icon } from 'semantic-ui-react'; + +class ControlPanel extends Component { + render() { + const colorSchemeOptions = [ + { + text: 'Black & White', + value: 'bw', + }, + { + text: 'Colors #1', + value: 'color1', + }, + { + text: 'Colors #2', + value: 'color2', + }, + { + text: 'Colors #3', + value: 'color3', + }, + { + text: 'Colors #4', + value: 'color4', + }, + ]; + return ( + + +

+ Control Panel +

+ this.props.changeColorScheme(data.value)} + selection + fluid + /> +
+
+ ); + } +} + +export default ControlPanel; diff --git a/src/components/GeoJSONLayer.js b/src/components/GeoJSONLayer.js new file mode 100644 index 0000000..bdf96f0 --- /dev/null +++ b/src/components/GeoJSONLayer.js @@ -0,0 +1,143 @@ +import React, { Component } from 'react'; +import { GeoJSON } from 'react-leaflet'; +import s2d from '../assets/s2d.json'; +import dioceseInfo from '../assets/diocese_info.json'; + +class GeoJSONLayer extends Component { + constructor(props) { + super(props); + this.state = { + geojson: null, + isLoading: true, + }; + this.geojsonRef = React.createRef(); + this.shapeToDiocese = s2d.map; + } + + componentDidMount() { + fetch(process.env.REACT_APP_GEOJSON_URL) + .then((response) => response.json()) + .then((data) => { + this.setState({ + geojson: data, + isLoading: false, + }); + }); + } + + getColor(shpfid) { + const { colorSchemes } = this.props.config; + const { currentColorScheme } = this.props; + const colors = colorSchemes[currentColorScheme]; + const { mappingData } = this.props; + + if (mappingData) { + const dioceseID = this.shapeToDiocese[shpfid]; + const maxNumEntries = this.props.maxNumEntries; + const numPerBucket = Math.ceil(maxNumEntries / colors.length); + if (mappingData.hasOwnProperty(dioceseID)) { + const numEntries = mappingData[dioceseID].length; + let index = Math.floor((numEntries - 1) / numPerBucket); + if (index > colors.length - 1) { + index = colors.length - 1; + } + return colors[index]; + } + } + return '#fff'; + } + + style = (feature) => { + return { + fillColor: this.getColor(feature.properties.SHPFID), + weight: 1, + opacity: 3, + color: 'grey', + dashArray: '3', + fillOpacity: 1.0, + }; + }; + + onEachFeature = (feature, layer) => { + layer.on({ + mouseover: this.highlightFeature, + mouseout: this.resetHighlight, + click: this.showRecordsModal, + }); + }; + + getDioceseData = (layer) => { + const { SHPFID: shpfid } = layer.feature.properties; + const { mappingData } = this.props; + const info = {}; + if (this.shapeToDiocese.hasOwnProperty(shpfid)) { + const dioceseID = this.shapeToDiocese[shpfid]; + if (dioceseInfo.hasOwnProperty(dioceseID)) { + const { + diocese_name, + diocese_alt, + province, + country_modern, + } = dioceseInfo[dioceseID]; + const diocese = diocese_alt + ? `${diocese_name} (${diocese_alt})` + : diocese_name; + info.diocese = diocese; + info.province = province; + info.country = country_modern; + } + + if (mappingData && mappingData.hasOwnProperty(dioceseID)) { + info.hasMappingData = true; + info.searchData = mappingData[dioceseID]; + info.query = mappingData.searchTerm; + } + } + return info; + }; + + highlightFeature = (e) => { + var layer = e.target; + const info = this.getDioceseData(layer); + this.props.updateInfo(info); + + layer.setStyle({ + weight: 2, + color: 'black', + dashArray: '', + fillOpacity: 1.0, + }); + + layer.bringToFront(); + }; + + resetHighlight = (e) => { + const { leafletElement } = this.geojsonRef.current; + leafletElement.resetStyle(e.target); + this.props.updateInfo(null); + }; + + showRecordsModal = (e) => { + var layer = e.target; + const info = this.getDioceseData(layer); + if (info.hasMappingData) { + this.props.showRecordsModal(info); + } + }; + + render() { + if (this.state.isLoading) { + return ; + } + return ( + + ); + } +} + +export default GeoJSONLayer; diff --git a/src/components/InfoPanel.js b/src/components/InfoPanel.js new file mode 100644 index 0000000..80586d1 --- /dev/null +++ b/src/components/InfoPanel.js @@ -0,0 +1,40 @@ +import React, { Component } from 'react'; +import { Card } from 'semantic-ui-react'; + +class InfoPanel extends Component { + render() { + const { info } = this.props; + const diocese = info ? info.diocese : 'Hover over a region'; + + // Province and Modern country + const attributes = ['province', 'country']; + const title = (str) => str.charAt(0).toUpperCase() + str.slice(1); + const provinceCountry = []; + if (info) { + for (let i = 0; i < attributes.length; i++) { + const attr = attributes[i]; + if (info.hasOwnProperty(attr)) { + provinceCountry.push( +
  • + {title(attr)}: {info[attr]} +
  • + ); + } + } + } + + return ( + + +

    {diocese}

    + {info &&
      {provinceCountry}
    } + {info && info.searchData && ( +
    Total hits: ({info.searchData.length})
    + )} +
    +
    + ); + } +} + +export default InfoPanel; diff --git a/src/components/Map.js b/src/components/Map.js deleted file mode 100644 index b711725..0000000 --- a/src/components/Map.js +++ /dev/null @@ -1,571 +0,0 @@ -import React, { Component } from 'react'; -import { - Map as LeafletMap, - TileLayer, - GeoJSON, - ScaleControl, -} from 'react-leaflet'; -import { - Button, - Card, - Dropdown, - Header, - Icon, - Label, - Modal, - Table, -} from 'semantic-ui-react'; -import mapConfig from '../assets/map_config'; -import s2d from '../assets/s2d.json'; -import metadataFields from '../assets/metadata_fields.json'; -import dioceseInfo from '../assets/diocese_info.json'; -import './Map.css'; - -//================= -// Mapbox base map -//================= - -class BaseMap extends React.Component { - render() { - const { tileLayer } = this.props.config; - return ( - - ); - } -} - -//================ -// Geo JSON layer -//================ - -class GeoJSONLayer extends React.Component { - constructor(props) { - super(props); - this.state = { - geojson: null, - isLoading: true, - }; - this.geojsonRef = React.createRef(); - this.shapeToDiocese = s2d.map; - this.onEachFeature = this.onEachFeature.bind(this); - this.highlightFeature = this.highlightFeature.bind(this); - this.resetHighlight = this.resetHighlight.bind(this); - this.showSearchResultsModal = this.showSearchResultsModal.bind(this); - this.style = this.style.bind(this); - this.getDioceseData = this.getDioceseData.bind(this); - } - - componentDidMount() { - fetch(process.env.REACT_APP_GEOJSON_URL) - .then((response) => response.json()) - .then((data) => { - this.setState({ - geojson: data, - isLoading: false, - }); - }); - } - - getColor(shpfid) { - const { colorSchemes } = this.props.config; - const { currentColorScheme } = this.props; - const colors = colorSchemes[currentColorScheme]; - const { mappingData } = this.props; - - if (mappingData) { - const dioceseID = this.shapeToDiocese[shpfid]; - const maxNumEntries = this.props.maxNumEntries; - const numPerBucket = Math.ceil(maxNumEntries / colors.length); - if (mappingData.hasOwnProperty(dioceseID)) { - const numEntries = mappingData[dioceseID].length; - let index = Math.floor((numEntries - 1) / numPerBucket); - if (index > colors.length - 1) { - index = colors.length - 1; - } - return colors[index]; - } - } - return '#fff'; - } - - style(feature) { - return { - fillColor: this.getColor(feature.properties.SHPFID), - weight: 1, - opacity: 3, - color: 'grey', - dashArray: '3', - fillOpacity: 1.0, - }; - } - - onEachFeature(feature, layer) { - layer.on({ - mouseover: this.highlightFeature, - mouseout: this.resetHighlight, - click: this.showSearchResultsModal, - }); - } - - getDioceseData(layer) { - const { SHPFID: shpfid } = layer.feature.properties; - const { mappingData } = this.props; - const info = {}; - if (this.shapeToDiocese.hasOwnProperty(shpfid)) { - const dioceseID = this.shapeToDiocese[shpfid]; - if (dioceseInfo.hasOwnProperty(dioceseID)) { - const { - diocese_name, - diocese_alt, - province, - country_modern, - } = dioceseInfo[dioceseID]; - const diocese = diocese_alt - ? `${diocese_name} (${diocese_alt})` - : diocese_name; - info.diocese = diocese; - info.province = province; - info.country = country_modern; - } - - if (mappingData && mappingData.hasOwnProperty(dioceseID)) { - info.hasMappingData = true; - info.searchData = mappingData[dioceseID]; - info.query = mappingData.searchTerm; - } - } - return info; - } - - highlightFeature(e) { - var layer = e.target; - const info = this.getDioceseData(layer); - this.props.updateInfo(info); - - layer.setStyle({ - weight: 2, - color: 'black', - dashArray: '', - fillOpacity: 1.0, - }); - - layer.bringToFront(); - } - - resetHighlight(e) { - const { leafletElement } = this.geojsonRef.current; - leafletElement.resetStyle(e.target); - this.props.updateInfo(null); - } - - showSearchResultsModal(e) { - var layer = e.target; - const info = this.getDioceseData(layer); - if (info.hasMappingData) { - this.props.showModal(info); - } - } - - render() { - if (this.state.isLoading) { - return ; - } - return ( - - ); - } -} - -//====================== -// Search Results Modal -//====================== - -class SearchResultsModal extends React.Component { - constructor(props) { - super(props); - this.state = { - show: new Array(1000).fill(false), - }; - this.toggleMetadataTable = this.toggleMetadataTable.bind(this); - this.handleClose = this.handleClose.bind(this); - } - - toggleMetadataTable(index) { - const newState = [...this.state.show]; - newState[index] = !newState[index]; - this.setState({ - show: newState, - }); - } - - handleClose() { - this.setState({ - show: new Array(1000).fill(false), - }); - this.props.closeModal(); - } - - render() { - const { searchResults } = this.props; - return ( - -
    - -
    - - - - - - -
    - ); - } -} - -const SearchResultsModalTitle = ({ searchResults }) => { - return searchResults ? ( - - - {searchResults.diocese} - - - ) : ( - '' - ); -}; - -const MetadataTable = ({ metadata }) => - metadataFields.map(({ id, label }, i) => { - if (metadata[id]) { - return ( - - {label} - {metadata[id]} - - ); - } - }); - -// prettier-ignore -const ResultCards = ({ searchResults, toggleMetadataTable, showTable }) => { - if (!searchResults || !searchResults.searchData) { - return
    ; - } - const baseURL = 'https://corpus-synodalium.com/philologic/corpus/query?report=concordance&method=proxy&start=0&end=0'; - return searchResults.searchData.map(({ context, metadata }, index) => { - const url = `${baseURL}&q=${searchResults.query}&record_id=%22${metadata.record_id}%22`; - const { origPlace, year, head } = metadata; - const label = `${index + 1}. ${origPlace} (${year}) - ${head}`; - return - toggleMetadataTable={toggleMetadataTable} - />; - }); -}; - -const SingleResultCard = (props) => ( -
    - - - -
    ... ${props.context} ...
    `, - }} - /> - - - {props.showTable[props.index] && ( - - {props.metadataTable} -
    - )} -
    -
    -
    -); - -//=============== -// Control Panel -//=============== - -class ControlPanel extends React.Component { - render() { - const colorSchemeOptions = [ - { - text: 'Black & White', - value: 'bw', - }, - { - text: 'Colors #1', - value: 'color1', - }, - { - text: 'Colors #2', - value: 'color2', - }, - { - text: 'Colors #3', - value: 'color3', - }, - { - text: 'Colors #4', - value: 'color4', - }, - ]; - return ( - - -

    - Control Panel -

    - this.props.changeColorScheme(data.value)} - selection - fluid - /> -
    -
    - ); - } -} - -//============ -// Info Panel -//============ - -class InfoPanel extends React.Component { - render() { - const { info } = this.props; - const diocese = info ? info.diocese : 'Hover over a region'; - - // Province and Modern country - const attributes = ['province', 'country']; - const title = (str) => str.charAt(0).toUpperCase() + str.slice(1); - const provinceCountry = []; - if (info) { - for (let i = 0; i < attributes.length; i++) { - const attr = attributes[i]; - if (info.hasOwnProperty(attr)) { - provinceCountry.push( -
  • - {title(attr)}: {info[attr]} -
  • - ); - } - } - } - - return ( - - -

    {diocese}

    - {info &&
      {provinceCountry}
    } - {info && info.searchData && ( -
    Total hits: ({info.searchData.length})
    - )} -
    -
    - ); - } -} - -//=============== -// Color Legend -//=============== - -class ColorLegend extends React.Component { - render() { - if (!this.props.mappingData) { - return null; - } - const { maxNumEntries, currentColorScheme } = this.props; - const { colorSchemes } = this.props.config; - const colors = colorSchemes[currentColorScheme]; - const numPerBucket = Math.ceil(maxNumEntries / colors.length); - - const getStyle = (colorHex) => ({ - background: colorHex, - }); - - const colorBlocks = []; - - colorBlocks.push( - - - 0 -
    -
    - ); - - for (let i = 0; i < colors.length; i++) { - const start = i * numPerBucket + 1; - const end = (i + 1) * numPerBucket; - let range = `${start} - ${end}`; - if (i === colors.length - 1) { - range = `> ${start}`; - } - colorBlocks.push( - - - {range} -
    -
    - ); - } - - return ( - - {colorBlocks} - - ); - } -} - -//==================== -// Main map component -//==================== - -class LocalLegislationMap extends Component { - constructor(props) { - super(props); - this.state = { - config: mapConfig, - currentColorScheme: 'color1', - info: null, - modalOpen: false, - searchResults: null, - }; - this.mapRef = React.createRef(); - this.changeColorScheme = this.changeColorScheme.bind(this); - this.updateInfo = this.updateInfo.bind(this); - this.showModal = this.showModal.bind(this); - this.closeModal = this.closeModal.bind(this); - } - - showModal(searchResults) { - this.setState({ - searchResults: searchResults, - modalOpen: true, - }); - } - - closeModal() { - this.setState({ modalOpen: false }); - } - - changeColorScheme(colorScheme) { - this.setState({ - currentColorScheme: colorScheme, - }); - } - - updateInfo(info) { - this.setState({ - info: info, - }); - } - - getMaxNumEntries = () => { - const { mappingData } = this.props; - let maxNumEntries = 0; - for (const prop in mappingData) { - if (mappingData.hasOwnProperty(prop)) { - const numEntries = mappingData[prop].length; - if (numEntries > maxNumEntries) { - maxNumEntries = numEntries; - } - } - } - return maxNumEntries; - }; - - render() { - const config = this.state.config; - const maxNumEntries = this.getMaxNumEntries(); - return ( -
    - - - - - - - - - -
    - ); - } -} - -export default LocalLegislationMap; diff --git a/src/components/MapBoxLayer.js b/src/components/MapBoxLayer.js new file mode 100644 index 0000000..33f5e59 --- /dev/null +++ b/src/components/MapBoxLayer.js @@ -0,0 +1,19 @@ +import React, { Component } from 'react'; +import { TileLayer } from 'react-leaflet'; + +class MapBoxLayer extends Component { + render() { + const { tileLayer } = this.props.config; + return ( + + ); + } +} + +export default MapBoxLayer; diff --git a/src/components/PasteURLModal.js b/src/components/PasteURLModal.js new file mode 100644 index 0000000..7ab0f28 --- /dev/null +++ b/src/components/PasteURLModal.js @@ -0,0 +1,99 @@ +import React, { Component } from 'react'; +import { Button, Modal, Header, Icon, Form } from 'semantic-ui-react'; + +class PasteURLModal extends Component { + constructor(props) { + super(props); + this.state = { + modalOpen: false, + url: this.props.inputURL, + }; + } + + handleOpen = () => this.setState({ modalOpen: true }); + + handleClose = () => this.setState({ modalOpen: false }); + + handleChange = (event) => this.setState({ url: event.target.value }); + + handleSubmit = (event) => { + this.props.handleInputURL(this.state.url); + this.setState({ modalOpen: false }); + this.setState({ url: '' }); + }; + + render() { + return ( + } + open={this.state.modalOpen} + onClose={this.handleClose} + size="small" + > +
    + + + + + + + + + ); + } +} + +const ModalButton = ({ handleOpen }) => ( + +); + +const URLInput = ({ url, handleChange }) => ( +
    + + + + +
    +); + +const Description = () => ( +
    + To map your search results, +
      +
    • + Go to the PhiloLogic database: or +
    • +
    • Make a search.
    • +
    • + Click "Map All Results" button on the top-right corner. (recommended) +
    • +
    • Alternatively, you can paste in the search URL below.
    • +
    +
    +); + +const Corpus = () => ( + + /corpus + +); + +const CorpusNorm = () => ( + + /corpusnorm + +); + +export default PasteURLModal; diff --git a/src/components/RecordsModal.js b/src/components/RecordsModal.js new file mode 100644 index 0000000..a2d4c36 --- /dev/null +++ b/src/components/RecordsModal.js @@ -0,0 +1,151 @@ +import React, { Component } from 'react'; +import { + Button, + Card, + Header, + Icon, + Label, + Modal, + Table, +} from 'semantic-ui-react'; +import metadataFields from '../assets/metadata_fields.json'; + +class RecordsModal extends Component { + constructor(props) { + super(props); + this.state = { + show: new Array(1000).fill(false), + }; + } + + toggleMetadataTable = (index) => { + const newState = [...this.state.show]; + newState[index] = !newState[index]; + this.setState({ + show: newState, + }); + }; + + handleClose = () => { + this.setState({ + show: new Array(1000).fill(false), + }); + this.props.closeRecordsModal(); + }; + + render() { + const { searchResults } = this.props; + return ( + +
    + + </Header> + <Modal.Content> + <CardList + searchResults={searchResults} + toggleMetadataTable={this.toggleMetadataTable} + showTable={this.state.show} + /> + </Modal.Content> + <Modal.Actions> + <Button color="blue" onClick={this.handleClose}> + <Icon name="checkmark" /> OK + </Button> + </Modal.Actions> + </Modal> + ); + } +} + +const Title = ({ searchResults }) => { + return searchResults ? ( + <span> + <Icon name="map marker alternate" /> + {searchResults.diocese} + <Label circular color="blue"> + {searchResults.searchData.length} + </Label> + </span> + ) : ( + '' + ); +}; + +const MetadataTable = ({ metadata }) => { + return metadataFields.map(({ id, label }, i) => { + // skip empty fields + if (!metadata[id]) { + return <Table.Row key={id} />; + } + return ( + <Table.Row key={id}> + <Table.Cell>{label}</Table.Cell> + <Table.Cell>{metadata[id]}</Table.Cell> + </Table.Row> + ); + }); +}; + +// prettier-ignore +const CardList = ({ searchResults, toggleMetadataTable, showTable }) => { + if (!searchResults || !searchResults.searchData) { + return <div />; + } + const baseURL = 'https://corpus-synodalium.com/philologic/corpus/query?report=concordance&method=proxy&start=0&end=0'; + return searchResults.searchData.map(({ context, metadata }, index) => { + const url = `${baseURL}&q=${searchResults.query}&record_id=%22${metadata.record_id}%22`; + const { origPlace, year, head } = metadata; + const label = `${index + 1}. ${origPlace} (${year}) - ${head}`; + return <SingleCard + key={index} + index={index} + context={context} + url={url} + label={label} + showTable={showTable} + metadataTable=<MetadataTable metadata={metadata}/> + toggleMetadataTable={toggleMetadataTable} + />; + }); +}; + +const SingleCard = (props) => ( + <div className="search-fragment-card"> + <Card fluid> + <Label className="search-fragment-header" attached="top"> + {props.label} + </Label> + <Card.Content> + <div + className="search-fragment-div" + dangerouslySetInnerHTML={{ + __html: `<div>... ${props.context} ...</div>`, + }} + /> + <Button icon labelPosition="left" href={props.url} target="_blank"> + <Icon name="search" /> + Show Record in PhiloLogic + </Button> + <Button + icon + labelPosition="left" + onClick={() => props.toggleMetadataTable(props.index)} + > + <Icon name="file alternate outline" /> + Show Metadata + </Button> + {props.showTable[props.index] && ( + <Table basic celled striped> + <Table.Body>{props.metadataTable}</Table.Body> + </Table> + )} + </Card.Content> + </Card> + </div> +); + +export default RecordsModal; diff --git a/src/components/TopMenuBar.js b/src/components/TopMenuBar.js new file mode 100644 index 0000000..f13296b --- /dev/null +++ b/src/components/TopMenuBar.js @@ -0,0 +1,34 @@ +import React, { Component } from 'react'; +import PasteURLModal from './PasteURLModal'; +import { Menu } from 'semantic-ui-react'; + +class TopMenuBar extends Component { + render() { + return ( + <Menu> + <Menu.Item> + <img src="/favicon.ico" alt="React Logo" /> + </Menu.Item> + + <Menu.Item name="map-action"> + <PasteURLModal + inputURL={this.props.inputURL} + handleInputURL={this.props.handleInputURL} + /> + </Menu.Item> + + {this.props.searchTerm && ( + <Menu.Item name="current-search"> + Current query: "{this.props.searchTerm}" + </Menu.Item> + )} + + <Menu.Item name="version" position="right"> + <a href="https://github.com/thawsitt/react-map/releases">v 0.4.2</a> + </Menu.Item> + </Menu> + ); + } +} + +export default TopMenuBar; diff --git a/src/index.js b/src/index.js index dcd7420..a3a5723 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import './index.css'; -import App from './components/App'; +import './styles/index.css'; +import App from './App'; import 'semantic-ui-css/semantic.min.css'; import registerServiceWorker from './registerServiceWorker'; diff --git a/src/components/App.css b/src/styles/App.css similarity index 95% rename from src/components/App.css rename to src/styles/App.css index 55e7778..032c841 100644 --- a/src/components/App.css +++ b/src/styles/App.css @@ -6,10 +6,6 @@ padding-bottom: 20px; } -code.example { - color: grey; -} - .center { position: absolute; z-index: 800; diff --git a/src/components/Map.css b/src/styles/Map.css similarity index 100% rename from src/components/Map.css rename to src/styles/Map.css diff --git a/src/index.css b/src/styles/index.css similarity index 100% rename from src/index.css rename to src/styles/index.css