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

View file

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

View file

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

View file

@ -1,43 +1,91 @@
import React from 'react'
import {connect} from 'react-redux'
import {List, Dropdown, Checkbox, Segment, Dimmer, Grid, Loader, Header, Message, Confirm} 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 React from "react";
import { connect } from "react-redux";
import {
List,
Dropdown,
Checkbox,
Segment,
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 {Page} from 'components'
import type {Track, TrackData, TrackComment} from 'types'
import {trackLayer, trackLayerRaw} from '../../mapstyles'
import api from "api";
import { Page } from "components";
import type { Track, TrackData, TrackComment } from "types";
import { trackLayer, trackLayerRaw } from "../../mapstyles";
import TrackActions from './TrackActions'
import TrackComments from './TrackComments'
import TrackDetails from './TrackDetails'
import TrackMap from './TrackMap'
import TrackActions from "./TrackActions";
import TrackComments from "./TrackComments";
import TrackDetails from "./TrackDetails";
import TrackMap from "./TrackMap";
import styles from './TrackPage.module.less'
import styles from "./TrackPage.module.less";
function useTriggerSubject() {
const subject$ = React.useMemo(() => new Subject(), [])
const trigger = React.useCallback(() => subject$.next(null), [subject$])
return [trigger, subject$]
const subject$ = React.useMemo(() => new Subject(), []);
const trigger = React.useCallback(() => subject$.next(null), [subject$]);
return [trigger, subject$];
}
function TrackMapSettings({showTrack, setShowTrack, pointsMode, setPointsMode, side, setSide}) {
function TrackMapSettings({
showTrack,
setShowTrack,
pointsMode,
setPointsMode,
side,
setSide,
}) {
return (
<>
<Header as="h4">Map settings</Header>
<List>
<List.Item>
<Checkbox checked={showTrack} 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}} />
<Checkbox
checked={showTrack}
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
</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
</div>
</List.Item>
@ -48,9 +96,17 @@ function TrackMapSettings({showTrack, setShowTrack, pointsMode, setPointsMode, s
value={pointsMode}
onChange={(e, d) => setPointsMode(d.value)}
options={[
{key: 'none', value: 'none', text: 'None'},
{key: 'overtakingEvents', value: 'overtakingEvents', text: 'Confirmed'},
{key: 'measurements', value: 'measurements', text: 'All measurements'},
{ key: "none", value: "none", text: "None" },
{
key: "overtakingEvents",
value: "overtakingEvents",
text: "Confirmed",
},
{
key: "measurements",
value: "measurements",
text: "All measurements",
},
]}
/>
</List.Item>
@ -61,204 +117,242 @@ function TrackMapSettings({showTrack, setShowTrack, pointsMode, setPointsMode, s
value={side}
onChange={(e, d) => setSide(d.value)}
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>
</>
)
);
}
const TrackPage = connect((state) => ({login: state.login}))(function TrackPage({login}) {
const {slug} = useParams()
const TrackPage = connect((state) => ({ login: state.login }))(
function TrackPage({ login }) {
const { slug } = useParams();
const [reloadComments, reloadComments$] = useTriggerSubject()
const history = useHistory()
const [reloadComments, reloadComments$] = useTriggerSubject();
const history = useHistory();
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),
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.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(
catchError(() => {
history.replace('/tracks')
return of(null);
})
)
)
),
pluck('track')
)
),
pluck("comments"),
startWith(null) // show track infos before comments are loaded
);
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
)
return combineLatest([track$, trackData$, comments$]).pipe(
map(([track, trackData, comments]) => ({
track,
trackData,
comments,
}))
);
},
null,
[slug]
);
const comments$ = concat(of(null), reloadComments$).pipe(
switchMap(() => slug$),
map((slug) => `/tracks/${slug}/comments`),
switchMap((url) =>
from(api.get(url)).pipe(
catchError(() => {
return of(null)
})
)
),
pluck('comments'),
startWith(null) // show track infos before comments are loaded
)
const onSubmitComment = React.useCallback(
async ({ body }) => {
await api.post(`/tracks/${slug}/comments`, {
body: { comment: { body } },
});
reloadComments();
},
[slug, reloadComments]
);
return combineLatest([track$, trackData$, comments$]).pipe(
map(([track, trackData, comments]) => ({track, trackData, comments}))
)
},
null,
[slug]
)
const onDeleteComment = React.useCallback(
async (id) => {
await api.delete(`/tracks/${slug}/comments/${id}`);
reloadComments();
},
[slug, reloadComments]
);
const onSubmitComment = React.useCallback(
async ({body}) => {
await api.post(`/tracks/${slug}/comments`, {
body: {comment: {body}},
})
reloadComments()
},
[slug, reloadComments]
)
const onDeleteComment = React.useCallback(
async (id) => {
await api.delete(`/tracks/${slug}/comments/${id}`)
reloadComments()
},
[slug, reloadComments]
)
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))
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 processing = ['processing', 'queued', 'created'].includes(track?.processingStatus)
const error = track?.processingStatus === 'error'
const loading = track == null || trackData === undefined;
const processing = ["processing", "queued", "created"].includes(
track?.processingStatus
);
const error = track?.processingStatus === "error";
const [showTrack, setShowTrack] = React.useState(true)
const [pointsMode, setPointsMode] = React.useState('overtakingEvents') // none|overtakingEvents|measurements
const [side, setSide] = React.useState('overtaker') // overtaker|stationary
const [showTrack, setShowTrack] = React.useState(true);
const [pointsMode, setPointsMode] = React.useState("overtakingEvents"); // none|overtakingEvents|measurements
const [side, setSide] = React.useState("overtaker"); // overtaker|stationary
const title = track ? track.title || 'Unnamed track' : null
return (
<Page
title={title}
stage={
<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}>
{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>
const title = track ? track.title || "Unnamed track" : null;
return (
<Page
title={title}
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}>
<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>
<TrackDetails {...{track, isAuthor}} />
<TrackActions {...{isAuthor, onDownload, slug}} />
<Header as="h2" dividing>
Description
</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
{...{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>
<TrackComments
{...{ hideLoader: loading, comments, login }}
onSubmit={onSubmitComment}
onDelete={onDeleteComment}
/>
</Container>
</>
}
>
<Confirm
open={downloadError != null}
cancelButton={false}
onConfirm={hideDownloadError}
header="Download failed"
content={String(downloadError)}
/>
</Page>
);
}
);
{/* <pre>{JSON.stringify(data, null, 2)}</pre> */}
</Page>
)
})
export default TrackPage
export default TrackPage;