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
<div style={{marginTop: 8}}> checked={showTrack}
<span style={{borderTop: '3px dashed ' + trackLayerRaw.paint['line-color'], height: 0, width: 24, display: 'inline-block', verticalAlign: 'middle', marginRight: 4}} /> onChange={(e, d) => setShowTrack(d.checked)}
/>{" "}
Show track
<div style={{ marginTop: 8 }}>
<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,204 +117,242 @@ 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) =>
concat( concat(
of(null), of(null),
from(api.get(url)).pipe(
catchError(() => {
history.replace("/tracks");
})
)
)
),
pluck("track")
);
const trackData$ = slug$.pipe(
map((slug) => `/tracks/${slug}/data`),
switchMap((url) =>
concat(
of(undefined),
from(api.get(url)).pipe(
catchError(() => {
return of(null);
})
)
)
),
startWith(undefined) // show track infos before track data is loaded
);
const comments$ = concat(of(null), reloadComments$).pipe(
switchMap(() => slug$),
map((slug) => `/tracks/${slug}/comments`),
switchMap((url) =>
from(api.get(url)).pipe( from(api.get(url)).pipe(
catchError(() => { catchError(() => {
history.replace('/tracks') return of(null);
}) })
) )
) ),
), pluck("comments"),
pluck('track') startWith(null) // show track infos before comments are loaded
) );
const trackData$ = slug$.pipe( return combineLatest([track$, trackData$, comments$]).pipe(
map((slug) => `/tracks/${slug}/data`), map(([track, trackData, comments]) => ({
switchMap((url) => track,
concat( trackData,
of(undefined), comments,
from(api.get(url)).pipe( }))
catchError(() => { );
return of(null) },
}) null,
) [slug]
) );
),
startWith(undefined) // show track infos before track data is loaded
)
const comments$ = concat(of(null), reloadComments$).pipe( const onSubmitComment = React.useCallback(
switchMap(() => slug$), async ({ body }) => {
map((slug) => `/tracks/${slug}/comments`), await api.post(`/tracks/${slug}/comments`, {
switchMap((url) => body: { comment: { body } },
from(api.get(url)).pipe( });
catchError(() => { reloadComments();
return of(null) },
}) [slug, reloadComments]
) );
),
pluck('comments'),
startWith(null) // show track infos before comments are loaded
)
return combineLatest([track$, trackData$, comments$]).pipe( const onDeleteComment = React.useCallback(
map(([track, trackData, comments]) => ({track, trackData, comments})) async (id) => {
) await api.delete(`/tracks/${slug}/comments/${id}`);
}, reloadComments();
null, },
[slug] [slug, reloadComments]
) );
const onSubmitComment = React.useCallback( const [downloadError, setDownloadError] = React.useState(null);
async ({body}) => { const hideDownloadError = React.useCallback(
await api.post(`/tracks/${slug}/comments`, { () => setDownloadError(null),
body: {comment: {body}}, [setDownloadError]
}) );
reloadComments() const onDownload = React.useCallback(
}, async (filename) => {
[slug, reloadComments] try {
) await api.downloadFile(`/tracks/${slug}/download/${filename}`);
} catch (err) {
const onDeleteComment = React.useCallback( if (/Failed to fetch/.test(String(err))) {
async (id) => { setDownloadError(
await api.delete(`/tracks/${slug}/comments/${id}`) "The track probably has not been imported correctly or recently enough. Please ask your administrator for assistance."
reloadComments() );
}, } else {
[slug, reloadComments] setDownloadError(String(err));
) }
const [downloadError, setDownloadError] = React.useState(null)
const hideDownloadError = React.useCallback(() => setDownloadError(null), [setDownloadError])
const onDownload = React.useCallback(
async (filename) => {
try {
await api.downloadFile(`/tracks/${slug}/download/${filename}`)
} catch (err) {
if (/Failed to fetch/.test(String(err))) {
setDownloadError(
'The track probably has not been imported correctly or recently enough. Please ask your administrator for assistance.'
)
} else {
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={
<div className={styles.stage}> <>
<Loader active={loading} /> <Container>
<Dimmer.Dimmable blurring dimmed={loading}>
<TrackMap {...{track, trackData, pointsMode, side, showTrack}} style={{height: '80vh'}} />
</Dimmer.Dimmable>
<div className={styles.details}>
{processing && (
<Message warning>
<Message.Content>Track data is still being processed, please reload page in a while.</Message.Content>
</Message>
)}
{error && (
<Message error>
<Message.Content>
The processing of this track failed, please ask your site administrator for help in debugging the
issue.
</Message.Content>
</Message>
)}
<Segment>
{track && ( {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}>
<Loader active={loading} />
<Dimmer.Dimmable blurring dimmed={loading}>
<TrackMap
{...{ track, trackData, pointsMode, side, showTrack }}
style={{ height: "80vh" }}
/>
</Dimmer.Dimmable>
<div className={styles.details}>
<Segment>
<TrackMapSettings
{...{
showTrack,
setShowTrack,
pointsMode,
setPointsMode,
side,
setSide,
}}
/>
</Segment>
{processing && (
<Message warning>
<Message.Content>
Track data is still being processed, please reload page in
a while.
</Message.Content>
</Message>
)}
{error && (
<Message error>
<Message.Content>
The processing of this track failed, please ask your site
administrator for help in debugging the issue.
</Message.Content>
</Message>
)}
</div>
</div>
<Container>
{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>
</div>
}
>
<Confirm
open={downloadError != null}
cancelButton={false}
onConfirm={hideDownloadError}
header="Download failed"
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 <TrackComments
{...{hideLoader: loading, comments, login}} {...{ hideLoader: loading, comments, login }}
onSubmit={onSubmitComment} onSubmit={onSubmitComment}
onDelete={onDeleteComment} onDelete={onDeleteComment}
/> />
</Grid.Column> </Container>
<Grid.Column width={4}> </>
<TrackMapSettings {...{showTrack, setShowTrack, pointsMode, setPointsMode, side, setSide}} /> }
</Grid.Column> >
</Grid.Row> <Confirm
</Grid> open={downloadError != null}
cancelButton={false}
onConfirm={hideDownloadError}
header="Download failed"
content={String(downloadError)}
/>
</Page>
);
}
);
{/* <pre>{JSON.stringify(data, null, 2)}</pre> */} export default TrackPage;
</Page>
)
})
export default TrackPage