Reorder Trackpage

This commit is contained in:
Paul Bienkowski 2022-07-24 19:32:48 +02:00
parent fe7d7ce274
commit ab6cc6f6d0
4 changed files with 297 additions and 204 deletions

View file

@ -22,7 +22,7 @@ function CommentForm({onSubmit}) {
export default function TrackComments({comments, onSubmit, onDelete, login, hideLoader}) { export default function TrackComments({comments, onSubmit, onDelete, login, hideLoader}) {
return ( return (
<Segment basic> <>
<Comment.Group> <Comment.Group>
<Header as="h2" dividing> <Header as="h2" dividing>
Comments Comments
@ -63,6 +63,6 @@ export default function TrackComments({comments, onSubmit, onDelete, login, hide
{login && comments != null && <CommentForm onSubmit={onSubmit} />} {login && comments != null && <CommentForm onSubmit={onSubmit} />}
</Comment.Group> </Comment.Group>
</Segment> </>
) )
} }

View file

@ -10,7 +10,7 @@ function formatDuration(seconds) {
export default function TrackDetails({track, isAuthor}) { export default function TrackDetails({track, isAuthor}) {
return ( return (
<List> <List horizontal relaxed>
{track.public != null && isAuthor && ( {track.public != null && isAuthor && (
<List.Item> <List.Item>
<List.Header>Visibility</List.Header> <List.Header>Visibility</List.Header>

View file

@ -12,7 +12,6 @@
top: 16px; top: 16px;
right: 16px; right: 16px;
max-height: calc(100% - 32px); max-height: calc(100% - 32px);
overflow: auto;
} }
} }

View file

@ -1,43 +1,91 @@
import React from 'react' import React from "react";
import {connect} from 'react-redux' import { connect } from "react-redux";
import {List, Dropdown, Checkbox, Segment, Dimmer, Grid, Loader, Header, Message, Confirm} from 'semantic-ui-react' import {
import {useParams, useHistory} from 'react-router-dom' List,
import {concat, combineLatest, of, from, Subject} from 'rxjs' Dropdown,
import {pluck, distinctUntilChanged, map, switchMap, startWith, catchError} from 'rxjs/operators' Checkbox,
import {useObservable} from 'rxjs-hooks' Segment,
import Markdown from 'react-markdown' Dimmer,
Grid,
Loader,
Header,
Message,
Confirm,
Container,
} from "semantic-ui-react";
import { useParams, useHistory } from "react-router-dom";
import { concat, combineLatest, of, from, Subject } from "rxjs";
import {
pluck,
distinctUntilChanged,
map,
switchMap,
startWith,
catchError,
} from "rxjs/operators";
import { useObservable } from "rxjs-hooks";
import Markdown from "react-markdown";
import api from 'api' import api from "api";
import {Page} from 'components' import { Page } from "components";
import type {Track, TrackData, TrackComment} from 'types' import type { Track, TrackData, TrackComment } from "types";
import {trackLayer, trackLayerRaw} from '../../mapstyles' import { trackLayer, trackLayerRaw } from "../../mapstyles";
import TrackActions from './TrackActions' import TrackActions from "./TrackActions";
import TrackComments from './TrackComments' import TrackComments from "./TrackComments";
import TrackDetails from './TrackDetails' import TrackDetails from "./TrackDetails";
import TrackMap from './TrackMap' import TrackMap from "./TrackMap";
import styles from './TrackPage.module.less' import styles from "./TrackPage.module.less";
function useTriggerSubject() { function useTriggerSubject() {
const subject$ = React.useMemo(() => new Subject(), []) const subject$ = React.useMemo(() => new Subject(), []);
const trigger = React.useCallback(() => subject$.next(null), [subject$]) const trigger = React.useCallback(() => subject$.next(null), [subject$]);
return [trigger, subject$] return [trigger, subject$];
} }
function TrackMapSettings({showTrack, setShowTrack, pointsMode, setPointsMode, side, setSide}) { function TrackMapSettings({
showTrack,
setShowTrack,
pointsMode,
setPointsMode,
side,
setSide,
}) {
return ( return (
<> <>
<Header as="h4">Map settings</Header> <Header as="h4">Map settings</Header>
<List> <List>
<List.Item> <List.Item>
<Checkbox checked={showTrack} onChange={(e, d) => setShowTrack(d.checked)} /> Show track <Checkbox
checked={showTrack}
onChange={(e, d) => setShowTrack(d.checked)}
/>{" "}
Show track
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<span style={{borderTop: '3px dashed ' + trackLayerRaw.paint['line-color'], height: 0, width: 24, display: 'inline-block', verticalAlign: 'middle', marginRight: 4}} /> <span
style={{
borderTop: "3px dashed " + trackLayerRaw.paint["line-color"],
height: 0,
width: 24,
display: "inline-block",
verticalAlign: "middle",
marginRight: 4,
}}
/>
GPS track GPS track
</div> </div>
<div> <div>
<span style={{borderTop: '6px solid ' + trackLayerRaw.paint['line-color'], height: 6, width: 24, display: 'inline-block', verticalAlign: 'middle', marginRight: 4}} /> <span
style={{
borderTop: "6px solid " + trackLayerRaw.paint["line-color"],
height: 6,
width: 24,
display: "inline-block",
verticalAlign: "middle",
marginRight: 4,
}}
/>
Snapped to road Snapped to road
</div> </div>
</List.Item> </List.Item>
@ -48,9 +96,17 @@ function TrackMapSettings({showTrack, setShowTrack, pointsMode, setPointsMode, s
value={pointsMode} value={pointsMode}
onChange={(e, d) => setPointsMode(d.value)} onChange={(e, d) => setPointsMode(d.value)}
options={[ options={[
{key: 'none', value: 'none', text: 'None'}, { key: "none", value: "none", text: "None" },
{key: 'overtakingEvents', value: 'overtakingEvents', text: 'Confirmed'}, {
{key: 'measurements', value: 'measurements', text: 'All measurements'}, key: "overtakingEvents",
value: "overtakingEvents",
text: "Confirmed",
},
{
key: "measurements",
value: "measurements",
text: "All measurements",
},
]} ]}
/> />
</List.Item> </List.Item>
@ -61,29 +117,38 @@ function TrackMapSettings({showTrack, setShowTrack, pointsMode, setPointsMode, s
value={side} value={side}
onChange={(e, d) => setSide(d.value)} onChange={(e, d) => setSide(d.value)}
options={[ options={[
{key: 'overtaker', value: 'overtaker', text: 'Overtaker (Left)'}, {
{key: 'stationary', value: 'stationary', text: 'Stationary (Right)'}, key: "overtaker",
value: "overtaker",
text: "Overtaker (Left)",
},
{
key: "stationary",
value: "stationary",
text: "Stationary (Right)",
},
]} ]}
/> />
</List.Item> </List.Item>
</List> </List>
</> </>
) );
} }
const TrackPage = connect((state) => ({login: state.login}))(function TrackPage({login}) { const TrackPage = connect((state) => ({ login: state.login }))(
const {slug} = useParams() function TrackPage({ login }) {
const { slug } = useParams();
const [reloadComments, reloadComments$] = useTriggerSubject() const [reloadComments, reloadComments$] = useTriggerSubject();
const history = useHistory() const history = useHistory();
const data: { const data: {
track: null | Track track: null | Track;
trackData: null | TrackData trackData: null | TrackData;
comments: null | TrackComment[] comments: null | TrackComment[];
} | null = useObservable( } | null = useObservable(
(_$, args$) => { (_$, args$) => {
const slug$ = args$.pipe(pluck(0), distinctUntilChanged()) const slug$ = args$.pipe(pluck(0), distinctUntilChanged());
const track$ = slug$.pipe( const track$ = slug$.pipe(
map((slug) => `/tracks/${slug}`), map((slug) => `/tracks/${slug}`),
switchMap((url) => switchMap((url) =>
@ -91,13 +156,13 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
of(null), of(null),
from(api.get(url)).pipe( from(api.get(url)).pipe(
catchError(() => { catchError(() => {
history.replace('/tracks') history.replace("/tracks");
}) })
) )
) )
), ),
pluck('track') pluck("track")
) );
const trackData$ = slug$.pipe( const trackData$ = slug$.pipe(
map((slug) => `/tracks/${slug}/data`), map((slug) => `/tracks/${slug}/data`),
@ -106,13 +171,13 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
of(undefined), of(undefined),
from(api.get(url)).pipe( from(api.get(url)).pipe(
catchError(() => { catchError(() => {
return of(null) return of(null);
}) })
) )
) )
), ),
startWith(undefined) // show track infos before track data is loaded startWith(undefined) // show track infos before track data is loaded
) );
const comments$ = concat(of(null), reloadComments$).pipe( const comments$ = concat(of(null), reloadComments$).pipe(
switchMap(() => slug$), switchMap(() => slug$),
@ -120,109 +185,162 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
switchMap((url) => switchMap((url) =>
from(api.get(url)).pipe( from(api.get(url)).pipe(
catchError(() => { catchError(() => {
return of(null) return of(null);
}) })
) )
), ),
pluck('comments'), pluck("comments"),
startWith(null) // show track infos before comments are loaded startWith(null) // show track infos before comments are loaded
) );
return combineLatest([track$, trackData$, comments$]).pipe( return combineLatest([track$, trackData$, comments$]).pipe(
map(([track, trackData, comments]) => ({track, trackData, comments})) map(([track, trackData, comments]) => ({
) track,
trackData,
comments,
}))
);
}, },
null, null,
[slug] [slug]
) );
const onSubmitComment = React.useCallback( const onSubmitComment = React.useCallback(
async ({ body }) => { async ({ body }) => {
await api.post(`/tracks/${slug}/comments`, { await api.post(`/tracks/${slug}/comments`, {
body: { comment: { body } }, body: { comment: { body } },
}) });
reloadComments() reloadComments();
}, },
[slug, reloadComments] [slug, reloadComments]
) );
const onDeleteComment = React.useCallback( const onDeleteComment = React.useCallback(
async (id) => { async (id) => {
await api.delete(`/tracks/${slug}/comments/${id}`) await api.delete(`/tracks/${slug}/comments/${id}`);
reloadComments() reloadComments();
}, },
[slug, reloadComments] [slug, reloadComments]
) );
const [downloadError, setDownloadError] = React.useState(null) const [downloadError, setDownloadError] = React.useState(null);
const hideDownloadError = React.useCallback(() => setDownloadError(null), [setDownloadError]) const hideDownloadError = React.useCallback(
() => setDownloadError(null),
[setDownloadError]
);
const onDownload = React.useCallback( const onDownload = React.useCallback(
async (filename) => { async (filename) => {
try { try {
await api.downloadFile(`/tracks/${slug}/download/${filename}`) await api.downloadFile(`/tracks/${slug}/download/${filename}`);
} catch (err) { } catch (err) {
if (/Failed to fetch/.test(String(err))) { if (/Failed to fetch/.test(String(err))) {
setDownloadError( setDownloadError(
'The track probably has not been imported correctly or recently enough. Please ask your administrator for assistance.' "The track probably has not been imported correctly or recently enough. Please ask your administrator for assistance."
) );
} else { } else {
setDownloadError(String(err)) setDownloadError(String(err));
} }
} }
}, },
[slug] [slug]
) );
const isAuthor = login?.username === data?.track?.author?.username const isAuthor = login?.username === data?.track?.author?.username;
const {track, trackData, comments} = data || {} const { track, trackData, comments } = data || {};
const loading = track == null || trackData === undefined const loading = track == null || trackData === undefined;
const processing = ['processing', 'queued', 'created'].includes(track?.processingStatus) const processing = ["processing", "queued", "created"].includes(
const error = track?.processingStatus === 'error' track?.processingStatus
);
const error = track?.processingStatus === "error";
const [showTrack, setShowTrack] = React.useState(true) const [showTrack, setShowTrack] = React.useState(true);
const [pointsMode, setPointsMode] = React.useState('overtakingEvents') // none|overtakingEvents|measurements const [pointsMode, setPointsMode] = React.useState("overtakingEvents"); // none|overtakingEvents|measurements
const [side, setSide] = React.useState('overtaker') // overtaker|stationary const [side, setSide] = React.useState("overtaker"); // overtaker|stationary
const title = track ? track.title || 'Unnamed track' : null const title = track ? track.title || "Unnamed track" : null;
return ( return (
<Page <Page
title={title} title={title}
stage={ stage={
<>
<Container>
{track && (
<Segment basic>
<div style={{display: 'flex', alignItems: 'baseline', marginBlockStart: 32, marginBlockEnd: 16}}>
<Header as="h1">{title}</Header>
<div style={{marginLeft: 'auto'}}>
<TrackActions {...{ isAuthor, onDownload, slug }} />
</div>
</div>
<div style={{marginBlockEnd: 16}}>
<TrackDetails {...{ track, isAuthor }} />
</div>
</Segment>
)}
</Container>
<div className={styles.stage}> <div className={styles.stage}>
<Loader active={loading} /> <Loader active={loading} />
<Dimmer.Dimmable blurring dimmed={loading}> <Dimmer.Dimmable blurring dimmed={loading}>
<TrackMap {...{track, trackData, pointsMode, side, showTrack}} style={{height: '80vh'}} /> <TrackMap
{...{ track, trackData, pointsMode, side, showTrack }}
style={{ height: "80vh" }}
/>
</Dimmer.Dimmable> </Dimmer.Dimmable>
<div className={styles.details}> <div className={styles.details}>
<Segment>
<TrackMapSettings
{...{
showTrack,
setShowTrack,
pointsMode,
setPointsMode,
side,
setSide,
}}
/>
</Segment>
{processing && ( {processing && (
<Message warning> <Message warning>
<Message.Content>Track data is still being processed, please reload page in a while.</Message.Content> <Message.Content>
Track data is still being processed, please reload page in
a while.
</Message.Content>
</Message> </Message>
)} )}
{error && ( {error && (
<Message error> <Message error>
<Message.Content> <Message.Content>
The processing of this track failed, please ask your site administrator for help in debugging the The processing of this track failed, please ask your site
issue. administrator for help in debugging the issue.
</Message.Content> </Message.Content>
</Message> </Message>
)} )}
</div>
</div>
<Segment> <Container>
{track && ( {track?.description && (
<> <>
<Header as="h1">{title}</Header> <Header as="h2" dividing>
<TrackDetails {...{track, isAuthor}} /> Description
<TrackActions {...{isAuthor, onDownload, slug}} /> </Header>
<Markdown>{track.description}</Markdown>
</> </>
)} )}
</Segment>
</div> <TrackComments
</div> {...{ hideLoader: loading, comments, login }}
onSubmit={onSubmitComment}
onDelete={onDeleteComment}
/>
</Container>
</>
} }
> >
<Confirm <Confirm
@ -232,33 +350,9 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
header="Download failed" header="Download failed"
content={String(downloadError)} content={String(downloadError)}
/> />
<Grid stackable>
<Grid.Row>
<Grid.Column width={12}>
{track?.description && (
<Segment basic>
<Header as="h2" dividing>
Description
</Header>
<Markdown>{track.description}</Markdown>
</Segment>
)}
<TrackComments
{...{hideLoader: loading, comments, login}}
onSubmit={onSubmitComment}
onDelete={onDeleteComment}
/>
</Grid.Column>
<Grid.Column width={4}>
<TrackMapSettings {...{showTrack, setShowTrack, pointsMode, setPointsMode, side, setSide}} />
</Grid.Column>
</Grid.Row>
</Grid>
{/* <pre>{JSON.stringify(data, null, 2)}</pre> */}
</Page> </Page>
) );
}) }
);
export default TrackPage export default TrackPage;