This commit is contained in:
Paul Bienkowski 2021-02-14 18:54:05 +01:00
parent 0e12898521
commit 2e7cdc24f3
11 changed files with 436 additions and 417 deletions

View file

@ -0,0 +1,20 @@
import {DateTime} from 'luxon'
export default function FormattedDate({date, relative = false}) {
if (date == null) {
return null
}
const dateTime =
typeof date === 'string' ? DateTime.fromISO(date) : date instanceof Date ? DateTime.fromJSDate(date) : date
let str
if (relative) {
str = dateTime.toRelative()
} else {
str = dateTime.toLocaleString(DateTime.DATETIME_MED)
}
return <span title={dateTime.toISO()}>{str}</span>
}

View file

@ -5,14 +5,20 @@ import OlView from 'ol/View'
import OlTileLayer from 'ol/layer/Tile'
import OlVectorLayer from 'ol/layer/Vector'
import OlGroupLayer from 'ol/layer/Group'
import {fromLonLat} from 'ol/proj'
import OSM from 'ol/source/OSM'
import proj4 from 'proj4';
import {register} from 'ol/proj/proj4';
import OlLayerSwitcher from 'ol-layerswitcher'
// Import styles for open layers + addons
import 'ol/ol.css'
import "ol-layerswitcher/dist/ol-layerswitcher.css";
// Prepare projection
proj4.defs('projLayer1', '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs');
register(proj4);
const MapContext = React.createContext()
const MapLayerContext = React.createContext()
@ -22,14 +28,7 @@ export function Map({children, ...props}) {
const [map, setMap] = React.useState(null)
React.useLayoutEffect(() => {
const map = new OlMap({
target: ref.current,
// view: new View({
// maxZoom: 22,
// center: fromLonLat([10, 51]),
// zoom: 5,
// }),
})
const map = new OlMap({target: ref.current})
setMap(map)

View file

@ -1,3 +1,4 @@
export {default as FormattedDate} from './FormattedDate'
export {default as LoginForm} from './LoginForm'
export {default as Map} from './Map'
export {default as Page} from './Page'

View file

@ -1,4 +1,5 @@
import React from 'react'
import {Settings} from 'luxon'
import ReactDOM from 'react-dom'
import 'semantic-ui-css/semantic.min.css'
import './index.css'
@ -14,9 +15,13 @@ const enhancer = compose(persistState(['login']))
const store = createStore(rootReducer, undefined, enhancer)
// TODO: remove
Settings.defaultLocale = 'de-DE'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

View file

@ -1,407 +0,0 @@
import React from 'react'
import {connect} from 'react-redux'
import {Link} from 'react-router-dom'
import {Segment, Dimmer, Form, Button, List, Grid, Loader, Header, Comment} from 'semantic-ui-react'
import {useParams} from 'react-router-dom'
import {concat, combineLatest, of, from} from 'rxjs'
import {pluck, distinctUntilChanged, map, switchMap, startWith} from 'rxjs/operators'
import {useObservable} from 'rxjs-hooks'
import {Settings, DateTime, Duration} from 'luxon'
import {Vector as VectorSource} from 'ol/source';
import {Geometry, LineString, Point} from 'ol/geom';
import Feature from 'ol/Feature';
import {fromLonLat} from 'ol/proj';
import proj4 from 'proj4';
import {register} from 'ol/proj/proj4';
import {Fill, Stroke, Style, Text, Circle} from 'ol/style';
import api from '../api'
import {Map, Page} from '../components'
import type {Track, TrackData, TrackComment} from '../types'
proj4.defs('projLayer1', '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs');
register(proj4);
// TODO: remove
Settings.defaultLocale = 'de-DE'
function formatDuration(seconds) {
return Duration.fromMillis((seconds ?? 0) * 1000).toFormat("h'h' mm'm'")
}
function FormattedDate({date, relative = false}) {
if (date == null) {
return null
}
const dateTime =
typeof date === 'string' ? DateTime.fromISO(date) : date instanceof Date ? DateTime.fromJSDate(date) : date
let str
if (relative) {
str = dateTime.toRelative()
} else {
str = dateTime.toLocaleString(DateTime.DATETIME_MED)
}
return <span title={dateTime.toISO()}>{str}</span>
}
function TrackDetails({track, isAuthor, trackData}) {
return (
<List>
{track.visible != null && isAuthor && (
<List.Item>
<List.Header>Visibility</List.Header>
{track.visible ? 'Public' : 'Private'}
</List.Item>
)}
{track.originalFileName != null && (
<List.Item>
<List.Header>Original Filename</List.Header>
<code>{track.originalFileName}</code>
</List.Item>
)}
{track.uploadedByUserAgent != null && (
<List.Item>
<List.Header>Uploaded with</List.Header>
{track.uploadedByUserAgent}
</List.Item>
)}
{track.duration == null && (
<List.Item>
<List.Header>Duration</List.Header>
{formatDuration(track.duration || 1402)}
</List.Item>
)}
{track.createdAt != null && (
<List.Item>
<List.Header>Uploaded on</List.Header>
<FormattedDate date={track.createdAt} />
</List.Item>
)}
<Loader active={track != null && trackData == null} inline="centered" style={{marginTop: 16, marginBottom: 16}} />
{trackData?.recordedAt != null && (
<List.Item>
<List.Header>Recorded on</List.Header>
<FormattedDate date={trackData.recordedAt} />
</List.Item>
)}
{trackData?.numEvents != null && (
<List.Item>
<List.Header>Confirmed events</List.Header>
{trackData.numEvents}
</List.Item>
)}
{trackData?.trackLength != null && (
<List.Item>
<List.Header>Length</List.Header>
{(trackData.trackLength / 1000).toFixed(2)} km
</List.Item>
)}
</List>
)
}
function TrackActions({slug}) {
return (
<Button.Group vertical>
<Link to={`/tracks/${slug}/edit`}>
<Button primary>Edit track</Button>
</Link>
</Button.Group>
)
}
function TrackComments({comments, login, hideLoader}) {
return (
<Segment basic>
<Comment.Group>
<Header as="h2" dividing>
Comments
</Header>
<Loader active={!hideLoader && comments == null} inline />
{comments?.map((comment: TrackComment) => (
<Comment key={comment.id}>
<Comment.Avatar src={comment.author.image} />
<Comment.Content>
<Comment.Author as="a">{comment.author.username}</Comment.Author>
<Comment.Metadata>
<div>
<FormattedDate date={comment.createdAt} relative />
</div>
</Comment.Metadata>
<Comment.Text>{comment.body}</Comment.Text>
</Comment.Content>
</Comment>
))}
{login && comments != null && (
<Form reply>
<Form.TextArea rows={4} />
<Button content="Post comment" labelPosition="left" icon="edit" primary />
</Form>
)}
</Comment.Group>
</Segment>
)
}
const isValidTrackPoint = (point: TrackPoint): boolean =>
point.latitude != null && point.longitude != null && (point.latitude !== 0 || point.longitude !== 0)
function TrackMap({track, trackData, ...props}) {
const {
trackVectorSource,
trackPointsD1,
trackPointsD2,
trackPointsUntaggedD1,
trackPointsUntaggedD2,
viewExtent,
} = React.useMemo(() => {
const trackPointsD1: Feature<Geometry>[] = []
const trackPointsD2: Feature<Geometry>[] = []
const trackPointsUntaggedD1: Feature<Geometry>[] = []
const trackPointsUntaggedD2: Feature<Geometry>[] = []
const points: Coordinate[] = []
const filteredPoints: TrackPoint[] = trackData?.points.filter(isValidTrackPoint) ?? []
for (const dataPoint of filteredPoints) {
const {longitude, latitude, flag, d1, d2} = dataPoint
const p = fromLonLat([longitude, latitude])
points.push(p)
const geometry = new Point(p)
if (flag && d1) {
trackPointsD1.push(new Feature({distance: d1, geometry}))
}
if (flag && d2) {
trackPointsD2.push(new Feature({distance: d2, geometry}))
}
if (!flag && d1) {
trackPointsUntaggedD1.push(new Feature({distance: d1, geometry}))
}
if (!flag && d2) {
trackPointsUntaggedD2.push(new Feature({distance: d2, geometry}))
}
}
//Simplify to 1 point per 2 meter
const trackVectorSource = new VectorSource({
features: [new Feature(new LineString(points).simplify(2))],
})
const viewExtent = points.length ? trackVectorSource.getExtent() : null
return {trackVectorSource, trackPointsD1, trackPointsD2, trackPointsUntaggedD1, trackPointsUntaggedD2, viewExtent}
}, [trackData?.points])
const trackLayerStyle = React.useMemo(
() =>
new Style({
stroke: new Stroke({
width: 3,
color: 'rgb(30,144,255)',
}),
}),
[]
)
return (
<Map {...props}>
<Map.TileLayer />
<Map.VectorLayer
visible
updateWhileAnimating={false}
updateWhileInteracting={false}
source={trackVectorSource}
style={trackLayerStyle}
/>
<Map.GroupLayer title="Tagged Points">
<PointLayer features={trackPointsD1} title="Left" visible={true} />
<PointLayer features={trackPointsD2} title="Right" visible={false} />
</Map.GroupLayer>
<Map.GroupLayer title="Untagged Points" fold="close" visible={false}>
<PointLayer features={trackPointsUntaggedD1} title="Left Untagged" visible={false} />
<PointLayer features={trackPointsUntaggedD2} title="Right Untagged" visible={false} />
</Map.GroupLayer>
<Map.View maxZoom={22} zoom={15} center={fromLonLat([9.1797, 48.7784])} />
<Map.FitView extent={viewExtent} />
<Map.LayerSwitcher groupSelectStyle='children' startActive activationMode='click' reverse={false} />
</Map>
)
}
function pointStyleFunction(feature, resolution) {
let distance = feature.get('distance')
let radius = 200 / resolution
return new Style({
image: new Circle({
radius: radius < 20 ? radius : 20,
fill: evaluateDistanceForFillColor(distance),
stroke: evaluateDistanceForStrokeColor(distance),
}),
text: createTextStyle(distance, resolution),
})
}
const evaluateDistanceForFillColor = function (distance) {
const redFill = new Fill({color: 'rgba(255, 0, 0, 0.2)'})
const orangeFill = new Fill({color: 'rgba(245,134,0,0.2)'})
const greenFill = new Fill({color: 'rgba(50, 205, 50, 0.2)'})
switch (evaluateDistanceColor(distance)) {
case 'red':
return redFill
case 'orange':
return orangeFill
case 'green':
return greenFill
}
}
const evaluateDistanceForStrokeColor = function (distance) {
const redStroke = new Stroke({color: 'rgb(255, 0, 0)'})
const orangeStroke = new Stroke({color: 'rgb(245,134,0)'})
const greenStroke = new Stroke({color: 'rgb(50, 205, 50)'})
switch (evaluateDistanceColor(distance)) {
case 'red':
return redStroke
case 'orange':
return orangeStroke
case 'green':
return greenStroke
}
}
const WARN_DISTANCE= 200
const MIN_DISTANCE= 150
const evaluateDistanceColor = function (distance) {
if (distance < MIN_DISTANCE) {
return 'red'
} else if (distance < WARN_DISTANCE) {
return 'orange'
} else {
return 'green'
}
}
const createTextStyle = function (distance, resolution) {
return new Text({
textAlign: 'center',
textBaseline: 'middle',
font: 'normal 18px/1 Arial',
text: resolution < 6 ? '' + distance : '',
fill: new Fill({color: evaluateDistanceColor(distance)}),
stroke: new Stroke({color: 'white', width: 2}),
offsetX: 0,
offsetY: 0,
})
}
function PointLayer({features, title, visible}) {
return <Map.VectorLayer {...{title, visible}} style={pointStyleFunction} source={new VectorSource({features})} />
}
const TrackPage = connect((state) => ({login: state.login}))(function TrackPage({login}) {
const {slug} = useParams()
const data: {
track: null | Track
trackData: null | TrackData
comments: null | TrackComments
} | null = useObservable(
(_$, args$) => {
const slug$ = args$.pipe(pluck(0), distinctUntilChanged())
const track$ = slug$.pipe(
map((slug) => '/tracks/' + slug),
switchMap((url) => concat(of(null), from(api.fetch(url)))),
pluck('track')
)
const trackData$ = slug$.pipe(
map((slug) => '/tracks/' + slug + '/data'),
switchMap((url) => concat(of(null), from(api.fetch(url)))),
pluck('trackData'),
startWith(null) // show track infos before track data is loaded
)
const comments$ = slug$.pipe(
map((slug) => '/tracks/' + slug + '/comments'),
switchMap((url) => concat(of(null), from(api.fetch(url)))),
pluck('comments'),
startWith(null) // show track infos before comments are loaded
)
return combineLatest([track$, trackData$, comments$]).pipe(
map(([track, trackData, comments]) => ({track, trackData, comments}))
)
},
null,
[slug]
)
const isAuthor = login?.username === data?.track?.author?.username
const {track, trackData, comments} = data || {}
const loading = track == null || trackData == null
return (
<Page>
<Grid stackable>
<Grid.Row>
<Grid.Column width={12}>
<div style={{position: 'relative'}}>
<Loader active={loading} />
<Dimmer.Dimmable blurring dimmed={loading}>
<TrackMap {...{track, trackData}} style={{height: '60vh', minHeight: 400}} />
</Dimmer.Dimmable>
</div>
</Grid.Column>
<Grid.Column width={4}>
<Segment>
{track && (
<>
<Header as="h1">{track.title}</Header>
<TrackDetails {...{track, trackData, isAuthor}} />
{isAuthor && <TrackActions {...{slug}} />}
</>
)}
</Segment>
</Grid.Column>
</Grid.Row>
</Grid>
<TrackComments {...{hideLoader: loading, comments, login}} />
{/* <pre>{JSON.stringify(data, null, 2)}</pre> */}
</Page>
)
})
export default TrackPage

View file

@ -0,0 +1,13 @@
import React from 'react'
import {Link} from 'react-router-dom'
import {Button} from 'semantic-ui-react'
export default function TrackActions({slug}) {
return (
<Button.Group vertical>
<Link to={`/tracks/${slug}/edit`}>
<Button primary>Edit track</Button>
</Link>
</Button.Group>
)
}

View file

@ -0,0 +1,40 @@
import React from 'react'
import {Segment, Form, Button, Loader, Header, Comment} from 'semantic-ui-react'
import {FormattedDate} from 'components'
export default function TrackComments({comments, login, hideLoader}) {
return (
<Segment basic>
<Comment.Group>
<Header as="h2" dividing>
Comments
</Header>
<Loader active={!hideLoader && comments == null} inline />
{comments?.map((comment: TrackComment) => (
<Comment key={comment.id}>
<Comment.Avatar src={comment.author.image} />
<Comment.Content>
<Comment.Author as="a">{comment.author.username}</Comment.Author>
<Comment.Metadata>
<div>
<FormattedDate date={comment.createdAt} relative />
</div>
</Comment.Metadata>
<Comment.Text>{comment.body}</Comment.Text>
</Comment.Content>
</Comment>
))}
{login && comments != null && (
<Form reply>
<Form.TextArea rows={4} />
<Button content="Post comment" labelPosition="left" icon="edit" primary />
</Form>
)}
</Comment.Group>
</Segment>
)
}

View file

@ -0,0 +1,73 @@
import React from 'react'
import {List, Loader} from 'semantic-ui-react'
import {Duration} from 'luxon'
import {FormattedDate} from 'components'
function formatDuration(seconds) {
return Duration.fromMillis((seconds ?? 0) * 1000).toFormat("h'h' mm'm'")
}
export default function TrackDetails({track, isAuthor, trackData}) {
return (
<List>
{track.visible != null && isAuthor && (
<List.Item>
<List.Header>Visibility</List.Header>
{track.visible ? 'Public' : 'Private'}
</List.Item>
)}
{track.originalFileName != null && (
<List.Item>
<List.Header>Original Filename</List.Header>
<code>{track.originalFileName}</code>
</List.Item>
)}
{track.uploadedByUserAgent != null && (
<List.Item>
<List.Header>Uploaded with</List.Header>
{track.uploadedByUserAgent}
</List.Item>
)}
{track.duration == null && (
<List.Item>
<List.Header>Duration</List.Header>
{formatDuration(track.duration || 1402)}
</List.Item>
)}
{track.createdAt != null && (
<List.Item>
<List.Header>Uploaded on</List.Header>
<FormattedDate date={track.createdAt} />
</List.Item>
)}
<Loader active={track != null && trackData == null} inline="centered" style={{marginTop: 16, marginBottom: 16}} />
{trackData?.recordedAt != null && (
<List.Item>
<List.Header>Recorded on</List.Header>
<FormattedDate date={trackData.recordedAt} />
</List.Item>
)}
{trackData?.numEvents != null && (
<List.Item>
<List.Header>Confirmed events</List.Header>
{trackData.numEvents}
</List.Item>
)}
{trackData?.trackLength != null && (
<List.Item>
<List.Header>Length</List.Header>
{(trackData.trackLength / 1000).toFixed(2)} km
</List.Item>
)}
</List>
)
}

View file

@ -0,0 +1,179 @@
import React from 'react'
import {Vector as VectorSource} from 'ol/source';
import {Geometry, LineString, Point} from 'ol/geom';
import Feature from 'ol/Feature';
import {fromLonLat} from 'ol/proj';
import {Fill, Stroke, Style, Text, Circle} from 'ol/style';
import {Map} from 'components'
import type {TrackData, TrackPoint} from 'types'
const isValidTrackPoint = (point: TrackPoint): boolean =>
point.latitude != null && point.longitude != null && (point.latitude !== 0 || point.longitude !== 0)
const WARN_DISTANCE= 200
const MIN_DISTANCE= 150
const evaluateDistanceColor = function (distance) {
if (distance < MIN_DISTANCE) {
return 'red'
} else if (distance < WARN_DISTANCE) {
return 'orange'
} else {
return 'green'
}
}
const evaluateDistanceForFillColor = function (distance) {
const redFill = new Fill({color: 'rgba(255, 0, 0, 0.2)'})
const orangeFill = new Fill({color: 'rgba(245,134,0,0.2)'})
const greenFill = new Fill({color: 'rgba(50, 205, 50, 0.2)'})
switch (evaluateDistanceColor(distance)) {
case 'red':
return redFill
case 'orange':
return orangeFill
case 'green':
return greenFill
}
}
const evaluateDistanceForStrokeColor = function (distance) {
const redStroke = new Stroke({color: 'rgb(255, 0, 0)'})
const orangeStroke = new Stroke({color: 'rgb(245,134,0)'})
const greenStroke = new Stroke({color: 'rgb(50, 205, 50)'})
switch (evaluateDistanceColor(distance)) {
case 'red':
return redStroke
case 'orange':
return orangeStroke
case 'green':
return greenStroke
}
}
const createTextStyle = function (distance, resolution) {
return new Text({
textAlign: 'center',
textBaseline: 'middle',
font: 'normal 18px/1 Arial',
text: resolution < 6 ? '' + distance : '',
fill: new Fill({color: evaluateDistanceColor(distance)}),
stroke: new Stroke({color: 'white', width: 2}),
offsetX: 0,
offsetY: 0,
})
}
function pointStyleFunction(feature, resolution) {
let distance = feature.get('distance')
let radius = 200 / resolution
return new Style({
image: new Circle({
radius: radius < 20 ? radius : 20,
fill: evaluateDistanceForFillColor(distance),
stroke: evaluateDistanceForStrokeColor(distance),
}),
text: createTextStyle(distance, resolution),
})
}
function PointLayer({features, title, visible}) {
return <Map.VectorLayer {...{title, visible}} style={pointStyleFunction} source={new VectorSource({features})} />
}
export default function TrackMap({trackData, ...props}: {trackData: TrackData}) {
const {
trackVectorSource,
trackPointsD1,
trackPointsD2,
trackPointsUntaggedD1,
trackPointsUntaggedD2,
viewExtent,
} = React.useMemo(() => {
const trackPointsD1: Feature<Geometry>[] = []
const trackPointsD2: Feature<Geometry>[] = []
const trackPointsUntaggedD1: Feature<Geometry>[] = []
const trackPointsUntaggedD2: Feature<Geometry>[] = []
const points: Coordinate[] = []
const filteredPoints: TrackPoint[] = trackData?.points.filter(isValidTrackPoint) ?? []
for (const dataPoint of filteredPoints) {
const {longitude, latitude, flag, d1, d2} = dataPoint
const p = fromLonLat([longitude, latitude])
points.push(p)
const geometry = new Point(p)
if (flag && d1) {
trackPointsD1.push(new Feature({distance: d1, geometry}))
}
if (flag && d2) {
trackPointsD2.push(new Feature({distance: d2, geometry}))
}
if (!flag && d1) {
trackPointsUntaggedD1.push(new Feature({distance: d1, geometry}))
}
if (!flag && d2) {
trackPointsUntaggedD2.push(new Feature({distance: d2, geometry}))
}
}
//Simplify to 1 point per 2 meter
const trackVectorSource = new VectorSource({
features: [new Feature(new LineString(points).simplify(2))],
})
const viewExtent = points.length ? trackVectorSource.getExtent() : null
return {trackVectorSource, trackPointsD1, trackPointsD2, trackPointsUntaggedD1, trackPointsUntaggedD2, viewExtent}
}, [trackData?.points])
const trackLayerStyle = React.useMemo(
() =>
new Style({
stroke: new Stroke({
width: 3,
color: 'rgb(30,144,255)',
}),
}),
[]
)
return (
<Map {...props}>
<Map.TileLayer />
<Map.VectorLayer
visible
updateWhileAnimating={false}
updateWhileInteracting={false}
source={trackVectorSource}
style={trackLayerStyle}
/>
<Map.GroupLayer title="Tagged Points">
<PointLayer features={trackPointsD1} title="Left" visible={true} />
<PointLayer features={trackPointsD2} title="Right" visible={false} />
</Map.GroupLayer>
<Map.GroupLayer title="Untagged Points" fold="close" visible={false}>
<PointLayer features={trackPointsUntaggedD1} title="Left Untagged" visible={false} />
<PointLayer features={trackPointsUntaggedD2} title="Right Untagged" visible={false} />
</Map.GroupLayer>
<Map.View maxZoom={22} zoom={15} center={fromLonLat([9.1797, 48.7784])} />
<Map.FitView extent={viewExtent} />
<Map.LayerSwitcher groupSelectStyle='children' startActive activationMode='click' reverse={false} />
</Map>
)
}

View file

@ -0,0 +1,95 @@
import React from 'react'
import {connect} from 'react-redux'
import {Segment, Dimmer, Grid, Loader, Header} from 'semantic-ui-react'
import {useParams} from 'react-router-dom'
import {concat, combineLatest, of, from} from 'rxjs'
import {pluck, distinctUntilChanged, map, switchMap, startWith} from 'rxjs/operators'
import {useObservable} from 'rxjs-hooks'
import api from 'api'
import {Page} from 'components'
import type {Track, TrackData, TrackComment} from 'types'
import TrackActions from './TrackActions'
import TrackComments from './TrackComments'
import TrackDetails from './TrackDetails'
import TrackMap from './TrackMap'
const TrackPage = connect((state) => ({login: state.login}))(function TrackPage({login}) {
const {slug} = useParams()
const data: {
track: null | Track
trackData: null | TrackData
comments: null | TrackComment[]
} | null = useObservable(
(_$, args$) => {
const slug$ = args$.pipe(pluck(0), distinctUntilChanged())
const track$ = slug$.pipe(
map((slug) => '/tracks/' + slug),
switchMap((url) => concat(of(null), from(api.fetch(url)))),
pluck('track')
)
const trackData$ = slug$.pipe(
map((slug) => '/tracks/' + slug + '/data'),
switchMap((url) => concat(of(null), from(api.fetch(url)))),
pluck('trackData'),
startWith(null) // show track infos before track data is loaded
)
const comments$ = slug$.pipe(
map((slug) => '/tracks/' + slug + '/comments'),
switchMap((url) => concat(of(null), from(api.fetch(url)))),
pluck('comments'),
startWith(null) // show track infos before comments are loaded
)
return combineLatest([track$, trackData$, comments$]).pipe(
map(([track, trackData, comments]) => ({track, trackData, comments}))
)
},
null,
[slug]
)
const isAuthor = login?.username === data?.track?.author?.username
const {track, trackData, comments} = data || {}
const loading = track == null || trackData == null
return (
<Page>
<Grid stackable>
<Grid.Row>
<Grid.Column width={12}>
<div style={{position: 'relative'}}>
<Loader active={loading} />
<Dimmer.Dimmable blurring dimmed={loading}>
<TrackMap {...{track, trackData}} style={{height: '60vh', minHeight: 400}} />
</Dimmer.Dimmable>
</div>
</Grid.Column>
<Grid.Column width={4}>
<Segment>
{track && (
<>
<Header as="h1">{track.title}</Header>
<TrackDetails {...{track, trackData, isAuthor}} />
{isAuthor && <TrackActions {...{slug}} />}
</>
)}
</Segment>
</Grid.Column>
</Grid.Row>
</Grid>
<TrackComments {...{hideLoader: loading, comments, login}} />
{/* <pre>{JSON.stringify(data, null, 2)}</pre> */}
</Page>
)
})
export default TrackPage

View file

@ -19,7 +19,8 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"downlevelIteration": true
"downlevelIteration": true,
"baseUrl": "src"
},
"include": [
"src"