frontend: Track editor and redirects on errors
This commit is contained in:
parent
eef5deca70
commit
7ab7e4918e
|
@ -11,6 +11,7 @@ import {
|
||||||
LogoutPage,
|
LogoutPage,
|
||||||
NotFoundPage,
|
NotFoundPage,
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
|
TrackEditor,
|
||||||
TrackPage,
|
TrackPage,
|
||||||
TracksPage,
|
TracksPage,
|
||||||
UploadPage,
|
UploadPage,
|
||||||
|
@ -40,7 +41,7 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
|
||||||
<Link to="/">Home</Link>
|
<Link to="/">Home</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/feed">Tracks</Link>
|
<Link to="/tracks">Tracks</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://openbikesensor.org/" target="_blank" rel="noreferrer">
|
<a href="https://openbikesensor.org/" target="_blank" rel="noreferrer">
|
||||||
|
@ -76,15 +77,18 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
|
||||||
<Route path="/" exact>
|
<Route path="/" exact>
|
||||||
<HomePage />
|
<HomePage />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/feed" exact>
|
<Route path="/tracks" exact>
|
||||||
<TracksPage />
|
<TracksPage />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/feed/my" exact>
|
<Route path="/my/tracks" exact>
|
||||||
<TracksPage privateFeed />
|
<TracksPage privateTracks />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`/tracks/:slug`} exact>
|
<Route path={`/tracks/:slug`} exact>
|
||||||
<TrackPage />
|
<TrackPage />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path={`/tracks/:slug/edit`} exact>
|
||||||
|
<TrackEditor />
|
||||||
|
</Route>
|
||||||
<Route path="/redirect" exact>
|
<Route path="/redirect" exact>
|
||||||
<LoginRedirectPage />
|
<LoginRedirectPage />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -198,6 +198,8 @@ class API {
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return json
|
return json
|
||||||
|
} else if (response.status === 204) {
|
||||||
|
return null
|
||||||
} else {
|
} else {
|
||||||
throw new RequestError('Error code ' + response.status, json?.errors)
|
throw new RequestError('Error code ' + response.status, json?.errors)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import _ from 'lodash'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {Link} from 'react-router-dom'
|
||||||
import {Message, Grid, Loader, Statistic, Segment, Header, Item} from 'semantic-ui-react'
|
import {Message, Grid, Loader, Statistic, Segment, Header, Item} from 'semantic-ui-react'
|
||||||
import {useObservable} from 'rxjs-hooks'
|
import {useObservable} from 'rxjs-hooks'
|
||||||
import {of, pipe, from} from 'rxjs'
|
import {of, from} from 'rxjs'
|
||||||
import {map, switchMap, distinctUntilChanged} from 'rxjs/operators'
|
import {map, switchMap} from 'rxjs/operators'
|
||||||
import {fromLonLat} from 'ol/proj'
|
import {fromLonLat} from 'ol/proj'
|
||||||
import {Duration} from 'luxon'
|
import {Duration} from 'luxon'
|
||||||
|
|
||||||
|
@ -30,10 +30,9 @@ function WelcomeMap() {
|
||||||
|
|
||||||
function Stats() {
|
function Stats() {
|
||||||
const stats = useObservable(
|
const stats = useObservable(
|
||||||
pipe(
|
() => of(null).pipe(
|
||||||
distinctUntilChanged(_.isEqual),
|
|
||||||
switchMap(() => api.fetch('/stats'))
|
switchMap(() => api.fetch('/stats'))
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -82,7 +81,9 @@ function MostRecentTrack() {
|
||||||
<h2>Most recent track</h2>
|
<h2>Most recent track</h2>
|
||||||
<Loader active={track === null} />
|
<Loader active={track === null} />
|
||||||
{track === undefined ? (
|
{track === undefined ? (
|
||||||
<Message>No track uploaded yet. Be the first!</Message>
|
<Message>
|
||||||
|
No track uploaded yet. <Link to="/upload">Be the first!</Link>
|
||||||
|
</Message>
|
||||||
) : track ? (
|
) : track ? (
|
||||||
<Item.Group>
|
<Item.Group>
|
||||||
<TrackListItem track={track} />
|
<TrackListItem track={track} />
|
||||||
|
|
|
@ -6,10 +6,8 @@ import {useForm} from 'react-hook-form'
|
||||||
import {setLogin} from 'reducers/login'
|
import {setLogin} from 'reducers/login'
|
||||||
import {Page} from 'components'
|
import {Page} from 'components'
|
||||||
import api from 'api'
|
import api from 'api'
|
||||||
|
import {findInput} from 'utils'
|
||||||
|
|
||||||
function findInput(register) {
|
|
||||||
return (element) => register(element ? element.querySelector('input, textarea, select') : null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsPage = connect((state) => ({login: state.login}), {setLogin})(function SettingsPage({login, setLogin}) {
|
const SettingsPage = connect((state) => ({login: state.login}), {setLogin})(function SettingsPage({login, setLogin}) {
|
||||||
const {register, handleSubmit} = useForm()
|
const {register, handleSubmit} = useForm()
|
||||||
|
|
154
frontend/src/pages/TrackEditor.tsx
Normal file
154
frontend/src/pages/TrackEditor.tsx
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import React from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import {connect} from 'react-redux'
|
||||||
|
import {Confirm, Grid, Button, Icon, Popup, Form, Ref, TextArea, Checkbox} from 'semantic-ui-react'
|
||||||
|
import {useHistory, useParams} from 'react-router-dom'
|
||||||
|
import {concat, of, from} from 'rxjs'
|
||||||
|
import {pluck, distinctUntilChanged, map, switchMap} from 'rxjs/operators'
|
||||||
|
import {useObservable} from 'rxjs-hooks'
|
||||||
|
import {findInput} from 'utils'
|
||||||
|
import {useForm, Controller} from 'react-hook-form'
|
||||||
|
|
||||||
|
import api from 'api'
|
||||||
|
import {Page} from 'components'
|
||||||
|
import type {Track} from 'types'
|
||||||
|
|
||||||
|
const TrackEditor = connect((state) => ({login: state.login}))(function TrackEditor({login}) {
|
||||||
|
const [busy, setBusy] = React.useState(false)
|
||||||
|
const {register, control, handleSubmit} = useForm()
|
||||||
|
const {slug} = useParams()
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const track: null | Track = useObservable(
|
||||||
|
(_$, args$) => {
|
||||||
|
const slug$ = args$.pipe(pluck(0), distinctUntilChanged())
|
||||||
|
return slug$.pipe(
|
||||||
|
map((slug) => `/tracks/${slug}`),
|
||||||
|
switchMap((url) => concat(of(null), from(api.get(url)))),
|
||||||
|
pluck('track')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
[slug]
|
||||||
|
)
|
||||||
|
|
||||||
|
const loading = busy || track == null
|
||||||
|
const isAuthor = login?.username === track?.author?.username
|
||||||
|
|
||||||
|
// Navigate to track detials if we are not the author
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!login || (track && !isAuthor)) {
|
||||||
|
history.replace(`/tracks/${slug}`)
|
||||||
|
}
|
||||||
|
}, [slug, login, track, isAuthor, history])
|
||||||
|
|
||||||
|
const onSubmit = React.useMemo(
|
||||||
|
() =>
|
||||||
|
handleSubmit(async (values) => {
|
||||||
|
setBusy(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.put(`/tracks/${slug}`, {body: {track: _.pickBy(values, (v) => typeof v !== 'undefined')}})
|
||||||
|
history.push(`/tracks/${slug}`)
|
||||||
|
} finally {
|
||||||
|
setBusy(false)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[slug, handleSubmit, history]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [confirmDelete, setConfirmDelete] = React.useState(false)
|
||||||
|
const onDelete = React.useCallback(async () => {
|
||||||
|
setBusy(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/tracks/${slug}`)
|
||||||
|
history.push('/tracks')
|
||||||
|
} finally {
|
||||||
|
setConfirmDelete(false)
|
||||||
|
setBusy(false)
|
||||||
|
}
|
||||||
|
}, [setBusy, setConfirmDelete, slug, history])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<Grid centered relaxed divided>
|
||||||
|
<Grid.Row>
|
||||||
|
<Grid.Column width={8}>
|
||||||
|
<h2>Edit track</h2>
|
||||||
|
<Form loading={loading} key={track?.slug} onSubmit={onSubmit}>
|
||||||
|
<Ref innerRef={findInput(register)}>
|
||||||
|
<Form.Input label="Title" name="title" defaultValue={track?.title} style={{fontSize: '120%'}} />
|
||||||
|
</Ref>
|
||||||
|
|
||||||
|
<Form.Field>
|
||||||
|
<label>Description</label>
|
||||||
|
<Ref innerRef={register}>
|
||||||
|
<TextArea name="description" rows={4} defaultValue={track?.description} />
|
||||||
|
</Ref>
|
||||||
|
</Form.Field>
|
||||||
|
|
||||||
|
<Form.Field>
|
||||||
|
<label>Visibility</label>
|
||||||
|
<Controller
|
||||||
|
name="visible"
|
||||||
|
control={control}
|
||||||
|
defaultValue={track?.visible}
|
||||||
|
render={(props) => (
|
||||||
|
<Checkbox
|
||||||
|
name="visible"
|
||||||
|
label="Make track visible in public track list"
|
||||||
|
checked={props.value}
|
||||||
|
onChange={(_, {checked}) => props.onChange(checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Popup
|
||||||
|
wide="very"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Checking this box allows all users to see your full track. For your own privacy and security,
|
||||||
|
make sure to only publish tracks in this way that do not let others deduce where you live, work,
|
||||||
|
or frequently stay. Your recording device might have useful privacy settings to not record
|
||||||
|
geolocation data near those places.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In the future, this site will allow you to redact privacy sensitive data in tracks, both
|
||||||
|
manually and automatically. Until then, you will have to rely on the features of your recording
|
||||||
|
device, or manually redact your files before upload.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
After checking this box, your data essentially becomes public. You understand that we cannot
|
||||||
|
control who potentially downloads this data and and keeps a copy, even if you delete it from
|
||||||
|
your account or anonymize it later.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>Use at your own risk.</b>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
trigger={<Icon name="warning sign" style={{marginLeft: 8}} color="orange" />}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<Button type="submit">Save</Button>
|
||||||
|
</Form>
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column width={4}>
|
||||||
|
<h2>Danger zone</h2>
|
||||||
|
<p>
|
||||||
|
You can remove this track from your account and the portal if you like. However, if at any point you have
|
||||||
|
published this track, we cannot guarantee that there are no versions of it in the public data repository,
|
||||||
|
or any copy thereof.
|
||||||
|
</p>
|
||||||
|
<Button color="red" onClick={() => setConfirmDelete(true)}>Delete</Button>
|
||||||
|
<Confirm open={confirmDelete} onCancel={() => setConfirmDelete(false)} onConfirm={onDelete} />
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default TrackEditor
|
|
@ -1,9 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import {Table, Checkbox, Segment, Dimmer, Grid, Loader, Header} from 'semantic-ui-react'
|
import {Table, Checkbox, Segment, Dimmer, Grid, Loader, Header} from 'semantic-ui-react'
|
||||||
import {useParams} from 'react-router-dom'
|
import {useParams, useHistory} from 'react-router-dom'
|
||||||
import {concat, combineLatest, of, from, Subject} from 'rxjs'
|
import {concat, combineLatest, of, from, Subject} from 'rxjs'
|
||||||
import {pluck, distinctUntilChanged, map, switchMap, startWith} from 'rxjs/operators'
|
import {pluck, distinctUntilChanged, map, switchMap, startWith, catchError} from 'rxjs/operators'
|
||||||
import {useObservable} from 'rxjs-hooks'
|
import {useObservable} from 'rxjs-hooks'
|
||||||
import Markdown from 'react-markdown'
|
import Markdown from 'react-markdown'
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
||||||
const {slug} = useParams()
|
const {slug} = useParams()
|
||||||
|
|
||||||
const [reloadComments, reloadComments$] = useTriggerSubject()
|
const [reloadComments, reloadComments$] = useTriggerSubject()
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
const data: {
|
const data: {
|
||||||
track: null | Track
|
track: null | Track
|
||||||
|
@ -36,13 +37,31 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
||||||
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) => concat(of(null), from(api.get(url)))),
|
switchMap((url) =>
|
||||||
|
concat(
|
||||||
|
of(null),
|
||||||
|
from(api.get(url)).pipe(
|
||||||
|
catchError(() => {
|
||||||
|
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`),
|
||||||
switchMap((url) => concat(of(null), from(api.get(url)))),
|
switchMap((url) =>
|
||||||
|
concat(
|
||||||
|
of(null),
|
||||||
|
from(api.get(url)).pipe(
|
||||||
|
catchError(() => {
|
||||||
|
history.replace('/tracks')
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
pluck('trackData'),
|
pluck('trackData'),
|
||||||
startWith(null) // show track infos before track data is loaded
|
startWith(null) // show track infos before track data is loaded
|
||||||
)
|
)
|
||||||
|
@ -50,7 +69,13 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
||||||
const comments$ = concat(of(null), reloadComments$).pipe(
|
const comments$ = concat(of(null), reloadComments$).pipe(
|
||||||
switchMap(() => slug$),
|
switchMap(() => slug$),
|
||||||
map((slug) => `/tracks/${slug}/comments`),
|
map((slug) => `/tracks/${slug}/comments`),
|
||||||
switchMap((url) => api.get(url)),
|
switchMap((url) =>
|
||||||
|
from(api.get(url)).pipe(
|
||||||
|
catchError(() => {
|
||||||
|
history.replace('/tracks')
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
pluck('comments'),
|
pluck('comments'),
|
||||||
startWith(null) // show track infos before comments are loaded
|
startWith(null) // show track infos before comments are loaded
|
||||||
)
|
)
|
||||||
|
@ -63,17 +88,23 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
||||||
[slug]
|
[slug]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSubmitComment = React.useCallback(async ({body}) => {
|
const onSubmitComment = React.useCallback(
|
||||||
await api.post(`/tracks/${slug}/comments`, {
|
async ({body}) => {
|
||||||
body: {comment: {body}},
|
await api.post(`/tracks/${slug}/comments`, {
|
||||||
})
|
body: {comment: {body}},
|
||||||
reloadComments()
|
})
|
||||||
}, [slug, reloadComments])
|
reloadComments()
|
||||||
|
},
|
||||||
|
[slug, reloadComments]
|
||||||
|
)
|
||||||
|
|
||||||
const onDeleteComment = React.useCallback(async (id) => {
|
const onDeleteComment = React.useCallback(
|
||||||
await api.delete(`/tracks/${slug}/comments/${id}`)
|
async (id) => {
|
||||||
reloadComments()
|
await api.delete(`/tracks/${slug}/comments/${id}`)
|
||||||
}, [slug, reloadComments])
|
reloadComments()
|
||||||
|
},
|
||||||
|
[slug, reloadComments]
|
||||||
|
)
|
||||||
|
|
||||||
const isAuthor = login?.username === data?.track?.author?.username
|
const isAuthor = login?.username === data?.track?.author?.username
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import {Item, Tab, Loader, Pagination, Icon} from 'semantic-ui-react'
|
import {Message, Item, Tab, Loader, Pagination, Icon} from 'semantic-ui-react'
|
||||||
import {useObservable} from 'rxjs-hooks'
|
import {useObservable} from 'rxjs-hooks'
|
||||||
import {Link, useHistory, useRouteMatch} from 'react-router-dom'
|
import {Link, useHistory, useRouteMatch} from 'react-router-dom'
|
||||||
import {of, from, concat} from 'rxjs'
|
import {of, from, concat} from 'rxjs'
|
||||||
|
@ -16,8 +16,8 @@ function TracksPageTabs() {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const panes = React.useMemo(
|
const panes = React.useMemo(
|
||||||
() => [
|
() => [
|
||||||
{menuItem: 'Public tracks', url: '/feed'},
|
{menuItem: 'Public tracks', url: '/tracks'},
|
||||||
{menuItem: 'My tracks', url: '/feed/my'},
|
{menuItem: 'My tracks', url: '/my/tracks'},
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
@ -29,13 +29,13 @@ function TracksPageTabs() {
|
||||||
[history, panes]
|
[history, panes]
|
||||||
)
|
)
|
||||||
|
|
||||||
const isFeedPage = useRouteMatch('/feed/my')
|
const isOwnTracksPage = useRouteMatch('/my/tracks')
|
||||||
const activeIndex = isFeedPage ? 1 : 0
|
const activeIndex = isOwnTracksPage ? 1 : 0
|
||||||
|
|
||||||
return <Tab menu={{secondary: true, pointing: true}} {...{panes, onTabChange, activeIndex}} />
|
return <Tab menu={{secondary: true, pointing: true}} {...{panes, onTabChange, activeIndex}} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function TrackList({privateFeed}: {privateFeed: boolean}) {
|
function TrackList({privateTracks}: {privateTracks: boolean}) {
|
||||||
const [page, setPage] = useQueryParam<number>('page', 1, Number)
|
const [page, setPage] = useQueryParam<number>('page', 1, Number)
|
||||||
|
|
||||||
const pageSize = 10
|
const pageSize = 10
|
||||||
|
@ -46,8 +46,8 @@ function TrackList({privateFeed}: {privateFeed: boolean}) {
|
||||||
} | null = useObservable(
|
} | null = useObservable(
|
||||||
(_$, inputs$) =>
|
(_$, inputs$) =>
|
||||||
inputs$.pipe(
|
inputs$.pipe(
|
||||||
map(([page, privateFeed]) => {
|
map(([page, privateTracks]) => {
|
||||||
const url = '/tracks' + (privateFeed ? '/feed' : '')
|
const url = '/tracks' + (privateTracks ? '/feed' : '')
|
||||||
const query = {limit: pageSize, offset: pageSize * (page - 1)}
|
const query = {limit: pageSize, offset: pageSize * (page - 1)}
|
||||||
return {url, query}
|
return {url, query}
|
||||||
}),
|
}),
|
||||||
|
@ -55,7 +55,7 @@ function TrackList({privateFeed}: {privateFeed: boolean}) {
|
||||||
switchMap((request) => concat(of(null), from(api.get(request.url, {query: request.query}))))
|
switchMap((request) => concat(of(null), from(api.get(request.url, {query: request.query}))))
|
||||||
),
|
),
|
||||||
null,
|
null,
|
||||||
[page, privateFeed]
|
[page, privateTracks]
|
||||||
)
|
)
|
||||||
|
|
||||||
const {tracks, tracksCount} = data || {tracks: [], tracksCount: 0}
|
const {tracks, tracksCount} = data || {tracks: [], tracksCount: 0}
|
||||||
|
@ -73,12 +73,16 @@ function TrackList({privateFeed}: {privateFeed: boolean}) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tracks && (
|
{tracks && tracks.length ? (
|
||||||
<Item.Group divided>
|
<Item.Group divided>
|
||||||
{tracks.map((track: Track) => (
|
{tracks.map((track: Track) => (
|
||||||
<TrackListItem key={track.slug} {...{track, privateFeed}} />
|
<TrackListItem key={track.slug} {...{track, privateTracks}} />
|
||||||
))}
|
))}
|
||||||
</Item.Group>
|
</Item.Group>
|
||||||
|
) : (
|
||||||
|
<Message>
|
||||||
|
No track uploaded yet. <Link to="/upload">Be the first!</Link>
|
||||||
|
</Message>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -92,7 +96,7 @@ function maxLength(t, max) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TrackListItem({track, privateFeed = false}) {
|
export function TrackListItem({track, privateTracks = false}) {
|
||||||
return (
|
return (
|
||||||
<Item key={track.slug}>
|
<Item key={track.slug}>
|
||||||
<Item.Image size="tiny" src={track.author.image} />
|
<Item.Image size="tiny" src={track.author.image} />
|
||||||
|
@ -106,7 +110,7 @@ export function TrackListItem({track, privateFeed = false}) {
|
||||||
<Item.Description>
|
<Item.Description>
|
||||||
<StripMarkdown>{maxLength(track.description, 200)}</StripMarkdown>
|
<StripMarkdown>{maxLength(track.description, 200)}</StripMarkdown>
|
||||||
</Item.Description>
|
</Item.Description>
|
||||||
{privateFeed && (
|
{privateTracks && (
|
||||||
<Item.Extra>
|
<Item.Extra>
|
||||||
{track.visible ? (
|
{track.visible ? (
|
||||||
<>
|
<>
|
||||||
|
@ -124,11 +128,11 @@ export function TrackListItem({track, privateFeed = false}) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const TracksPage = connect((state) => ({login: (state as any).login}))(function TracksPage({login, privateFeed}) {
|
const TracksPage = connect((state) => ({login: (state as any).login}))(function TracksPage({login, privateTracks}) {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
{login ? <TracksPageTabs /> : null}
|
{login ? <TracksPageTabs /> : null}
|
||||||
<TrackList {...{privateFeed}} />
|
<TrackList {...{privateTracks}} />
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,7 @@ export {default as LoginRedirectPage} from './LoginRedirectPage'
|
||||||
export {default as LogoutPage} from './LogoutPage'
|
export {default as LogoutPage} from './LogoutPage'
|
||||||
export {default as NotFoundPage} from './NotFoundPage'
|
export {default as NotFoundPage} from './NotFoundPage'
|
||||||
export {default as SettingsPage} from './SettingsPage'
|
export {default as SettingsPage} from './SettingsPage'
|
||||||
|
export {default as TrackEditor} from './TrackEditor'
|
||||||
export {default as TrackPage} from './TrackPage'
|
export {default as TrackPage} from './TrackPage'
|
||||||
export {default as TracksPage} from './TracksPage'
|
export {default as TracksPage} from './TracksPage'
|
||||||
export {default as UploadPage} from './UploadPage'
|
export {default as UploadPage} from './UploadPage'
|
||||||
|
|
9
frontend/src/utils.js
Normal file
9
frontend/src/utils.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// Wraps the register callback from useForm into a new ref function, such that
|
||||||
|
// any child of the provided element that is an input component will be
|
||||||
|
// registered.
|
||||||
|
export function findInput(register) {
|
||||||
|
return (element) => {
|
||||||
|
const found = element ? element.querySelector('input, textarea, select, checkbox') : null
|
||||||
|
register(found)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue