prettier
This commit is contained in:
parent
ec2d5bcf77
commit
66e00359a9
11 changed files with 198 additions and 174 deletions
|
@ -31,7 +31,9 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
|
|||
<Link to="/feed">Feed</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://openbikesensor.org/" target="_blank">About</a>
|
||||
<a href="https://openbikesensor.org/" target="_blank">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
{login ? (
|
||||
<>
|
||||
|
@ -65,7 +67,7 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
|
|||
<TracksPage />
|
||||
</Route>
|
||||
<Route path="/feed/my" exact>
|
||||
<TracksPage privateFeed />
|
||||
<TracksPage privateFeed />
|
||||
</Route>
|
||||
<Route path={`/tracks/:slug`} exact>
|
||||
<TrackPage />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "styles.scss";
|
||||
@import 'styles.scss';
|
||||
|
||||
.App {
|
||||
text-align: center;
|
||||
|
@ -12,7 +12,7 @@
|
|||
}
|
||||
|
||||
.pageTitle {
|
||||
font-family: "Roboto Slab";
|
||||
font-family: 'Roboto Slab';
|
||||
font-weight: 600;
|
||||
font-size: 18pt;
|
||||
}
|
||||
|
@ -46,4 +46,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,18 +40,18 @@ const LoginForm = connect(
|
|||
dispatchLogin,
|
||||
])
|
||||
|
||||
return loggedIn ? null :(
|
||||
<Form className={className} onSubmit={onSubmit}>
|
||||
<Form.Field>
|
||||
<label>e-Mail</label>
|
||||
<input value={email} onChange={onChangeEmail} />
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Password</label>
|
||||
<input type="password" value={password} onChange={onChangePassword} />
|
||||
</Form.Field>
|
||||
<Button type="submit">Submit</Button>
|
||||
</Form>
|
||||
return loggedIn ? null : (
|
||||
<Form className={className} onSubmit={onSubmit}>
|
||||
<Form.Field>
|
||||
<label>e-Mail</label>
|
||||
<input value={email} onChange={onChangeEmail} />
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Password</label>
|
||||
<input type="password" value={password} onChange={onChangePassword} />
|
||||
</Form.Field>
|
||||
<Button type="submit">Submit</Button>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import React from 'react'
|
||||
|
||||
import OlMap from 'ol/Map';
|
||||
import View from 'ol/View';
|
||||
import OlTileLayer from 'ol/layer/Tile';
|
||||
import {fromLonLat} from 'ol/proj';
|
||||
import OSM from 'ol/source/OSM';
|
||||
|
||||
import "ol/ol.css";
|
||||
import OlMap from 'ol/Map'
|
||||
import View from 'ol/View'
|
||||
import OlTileLayer from 'ol/layer/Tile'
|
||||
import {fromLonLat} from 'ol/proj'
|
||||
import OSM from 'ol/source/OSM'
|
||||
|
||||
import 'ol/ol.css'
|
||||
|
||||
const MapContext = React.createContext()
|
||||
|
||||
|
@ -22,9 +21,9 @@ export function Map({children, ...props}) {
|
|||
view: new View({
|
||||
maxZoom: 22,
|
||||
center: fromLonLat([10, 51]),
|
||||
zoom: 5
|
||||
})
|
||||
});
|
||||
zoom: 5,
|
||||
}),
|
||||
})
|
||||
|
||||
setMap(map)
|
||||
|
||||
|
@ -34,29 +33,32 @@ export function Map({children, ...props}) {
|
|||
}
|
||||
}, [])
|
||||
|
||||
return <>
|
||||
<div ref={ref} {...props}>
|
||||
<MapContext.Provider value={map}>
|
||||
{children}
|
||||
</MapContext.Provider>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div ref={ref} {...props}>
|
||||
<MapContext.Provider value={map}>{children}</MapContext.Provider>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function TileLayer() {
|
||||
const map = React.useContext(MapContext)
|
||||
|
||||
const layer = React.useMemo(() => new OlTileLayer({
|
||||
source: new OSM()
|
||||
}), [])
|
||||
const layer = React.useMemo(
|
||||
() =>
|
||||
new OlTileLayer({
|
||||
source: new OSM(),
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
map?.addLayer(layer)
|
||||
return () => map?.removeLayer(layer)
|
||||
})
|
||||
return null
|
||||
|
||||
}
|
||||
|
||||
Map.TileLayer = TileLayer
|
||||
export default Map;
|
||||
export default Map
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "../../styles.scss";
|
||||
@import '../../styles.scss';
|
||||
|
||||
.page {
|
||||
@include container;
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: "Noto Sans", "Roboto", -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-family: 'Noto Sans', 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Oxygen', 'Ubuntu', 'Cantarell',
|
||||
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Noto Sans Mono", source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
font-family: 'Noto Sans Mono', source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
|
|
@ -7,38 +7,43 @@ import {of, pipe, from} from 'rxjs'
|
|||
import {map, switchMap, distinctUntilChanged} from 'rxjs/operators'
|
||||
import {Duration} from 'luxon'
|
||||
|
||||
|
||||
import api from '../api'
|
||||
import {Map, Page, LoginForm } from '../components'
|
||||
import {Map, Page, LoginForm} from '../components'
|
||||
|
||||
import {TrackListItem} from './TracksPage'
|
||||
|
||||
function formatDuration(seconds) {
|
||||
return Duration.fromMillis((seconds ?? 0)* 1000).as('hours').toFixed(1)
|
||||
return Duration.fromMillis((seconds ?? 0) * 1000)
|
||||
.as('hours')
|
||||
.toFixed(1)
|
||||
}
|
||||
|
||||
|
||||
function WelcomeMap() {
|
||||
return <Map style={{height: '24rem', backgroundColor: '#FEFEF4'}}>
|
||||
<Map.TileLayer />
|
||||
</Map>
|
||||
return (
|
||||
<Map style={{height: '24rem', backgroundColor: '#FEFEF4'}}>
|
||||
<Map.TileLayer />
|
||||
</Map>
|
||||
)
|
||||
}
|
||||
|
||||
function Stats() {
|
||||
const stats = useObservable(pipe(
|
||||
distinctUntilChanged(_.isEqual),
|
||||
switchMap(() => api.fetch('/stats')),
|
||||
))
|
||||
const stats = useObservable(
|
||||
pipe(
|
||||
distinctUntilChanged(_.isEqual),
|
||||
switchMap(() => api.fetch('/stats'))
|
||||
)
|
||||
)
|
||||
|
||||
return <>
|
||||
<Header as='h2'>Statistics</Header>
|
||||
return (
|
||||
<>
|
||||
<Header as="h2">Statistics</Header>
|
||||
|
||||
<Segment>
|
||||
<Loader active={stats == null} />
|
||||
|
||||
<Statistic.Group widths={4} size="tiny">
|
||||
<Statistic>
|
||||
<Statistic.Value>{Number(stats?.publicTrackLength/1000).toFixed(1)}</Statistic.Value>
|
||||
<Statistic.Value>{Number(stats?.publicTrackLength / 1000).toFixed(1)}</Statistic.Value>
|
||||
<Statistic.Label>km track length</Statistic.Label>
|
||||
</Statistic>
|
||||
<Statistic>
|
||||
|
@ -56,29 +61,28 @@ function Stats() {
|
|||
</Statistic.Group>
|
||||
</Segment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const LoginState = connect(
|
||||
(state) => ({login: state.login}),
|
||||
)(function LoginState({login}) {
|
||||
const LoginState = connect((state) => ({login: state.login}))(function LoginState({login}) {
|
||||
return login ? (
|
||||
<>
|
||||
<Header as='h2'>Logged in as {login.username} </Header>
|
||||
<Header as="h2">Logged in as {login.username} </Header>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Header as='h2'>Login</Header>
|
||||
<Header as="h2">Login</Header>
|
||||
<LoginForm />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
function MostRecentTrack() {
|
||||
const track: Track|null = useObservable(
|
||||
() => of(null).pipe(
|
||||
switchMap(() => from(api.fetch('/tracks?limit=1'))),
|
||||
map(({tracks}) => tracks[0]),
|
||||
const track: Track | null = useObservable(
|
||||
() =>
|
||||
of(null).pipe(
|
||||
switchMap(() => from(api.fetch('/tracks?limit=1'))),
|
||||
map(({tracks}) => tracks[0])
|
||||
),
|
||||
null,
|
||||
[]
|
||||
|
@ -86,11 +90,19 @@ function MostRecentTrack() {
|
|||
|
||||
console.log(track)
|
||||
|
||||
return <>
|
||||
<h2>Most recent track</h2>
|
||||
<Loader active={track === null} />
|
||||
{track === undefined ? <Message>No track uploaded yet. Be the first!</Message> : track ? <Item.Group><TrackListItem track={track} /></Item.Group> : null}
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<h2>Most recent track</h2>
|
||||
<Loader active={track === null} />
|
||||
{track === undefined ? (
|
||||
<Message>No track uploaded yet. Be the first!</Message>
|
||||
) : track ? (
|
||||
<Item.Group>
|
||||
<TrackListItem track={track} />
|
||||
</Item.Group>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
|
@ -112,7 +124,6 @@ export default function HomePage() {
|
|||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,9 +5,7 @@ import {Redirect} from 'react-router-dom'
|
|||
import styles from './LoginPage.module.scss'
|
||||
import {Page, LoginForm} from '../components'
|
||||
|
||||
const LoginPage = connect(
|
||||
(state) => ({loggedIn: Boolean(state.login)}),
|
||||
)(function LoginPage({loggedIn}) {
|
||||
const LoginPage = connect((state) => ({loggedIn: Boolean(state.login)}))(function LoginPage({loggedIn}) {
|
||||
return loggedIn ? (
|
||||
<Redirect to="/" />
|
||||
) : (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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 {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'
|
||||
|
@ -19,12 +19,13 @@ function formatDuration(seconds) {
|
|||
return Duration.fromMillis((seconds ?? 0) * 1000).toFormat("h'h' mm'm'")
|
||||
}
|
||||
|
||||
function FormattedDate({date, relative=false}) {
|
||||
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
|
||||
const dateTime =
|
||||
typeof date === 'string' ? DateTime.fromISO(date) : date instanceof Date ? DateTime.fromJSDate(date) : date
|
||||
|
||||
let str
|
||||
|
||||
|
@ -75,7 +76,7 @@ function TrackDetails({track, isAuthor, trackData}) {
|
|||
</List.Item>
|
||||
)}
|
||||
|
||||
<Loader active={track != null && trackData == null} inline='centered' style={{marginTop: 16, marginBottom: 16}} />
|
||||
<Loader active={track != null && trackData == null} inline="centered" style={{marginTop: 16, marginBottom: 16}} />
|
||||
|
||||
{trackData?.recordedAt != null && (
|
||||
<List.Item>
|
||||
|
@ -114,33 +115,36 @@ function TrackActions({slug}) {
|
|||
function TrackComments({comments, login, hideLoader}) {
|
||||
return (
|
||||
<Segment basic>
|
||||
<Comment.Group>
|
||||
<Header as="h2" dividing>
|
||||
Comments
|
||||
</Header>
|
||||
<Comment.Group>
|
||||
<Header as="h2" dividing>
|
||||
Comments
|
||||
</Header>
|
||||
|
||||
<Loader active={!hideLoader && comments == null} inline />
|
||||
<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>
|
||||
))}
|
||||
{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>
|
||||
{login && comments != null && (
|
||||
<Form reply>
|
||||
<Form.TextArea rows={4} />
|
||||
<Button content="Post comment" labelPosition="left" icon="edit" primary />
|
||||
</Form>
|
||||
)}
|
||||
</Comment.Group>
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -194,24 +198,24 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
|||
<Grid.Row>
|
||||
<Grid.Column width={12}>
|
||||
<div style={{position: 'relative'}}>
|
||||
<Loader active={loading} />
|
||||
<Dimmer.Dimmable blurring dimmed={loading}>
|
||||
<Map style={{height: '60vh', minHeight: 400}}>
|
||||
<Map.TileLayer />
|
||||
</Map>
|
||||
</Dimmer.Dimmable>
|
||||
</div>
|
||||
<Loader active={loading} />
|
||||
<Dimmer.Dimmable blurring dimmed={loading}>
|
||||
<Map style={{height: '60vh', minHeight: 400}}>
|
||||
<Map.TileLayer />
|
||||
</Map>
|
||||
</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>
|
||||
{track && (
|
||||
<>
|
||||
<Header as="h1">{track.title}</Header>
|
||||
<TrackDetails {...{track, trackData, isAuthor}} />
|
||||
{isAuthor && <TrackActions {...{slug}} />}
|
||||
</>
|
||||
)}
|
||||
</Segment>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
|
|
|
@ -42,8 +42,8 @@ function TrackList({privateFeed}: {privateFeed: boolean}) {
|
|||
const pageSize = 10
|
||||
|
||||
const data: {
|
||||
tracks: Track[],
|
||||
tracksCount: number,
|
||||
tracks: Track[]
|
||||
tracksCount: number
|
||||
} | null = useObservable(
|
||||
(_$, inputs$) =>
|
||||
inputs$.pipe(
|
||||
|
@ -66,12 +66,18 @@ function TrackList({privateFeed}: {privateFeed: boolean}) {
|
|||
return (
|
||||
<div>
|
||||
<Loader content="Loading" active={loading} />
|
||||
{!loading && totalPages > 1 && <Pagination activePage={page} onPageChange={(e, data) => setPage(data.activePage as number)} totalPages={totalPages} />}
|
||||
{!loading && totalPages > 1 && (
|
||||
<Pagination
|
||||
activePage={page}
|
||||
onPageChange={(e, data) => setPage(data.activePage as number)}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tracks && (
|
||||
<Item.Group divided>
|
||||
{tracks.map((track: Track) => (
|
||||
<TrackListItem key={track.slug} {...{track, privateFeed}} />
|
||||
<TrackListItem key={track.slug} {...{track, privateFeed}} />
|
||||
))}
|
||||
</Item.Group>
|
||||
)}
|
||||
|
@ -80,27 +86,33 @@ function TrackList({privateFeed}: {privateFeed: boolean}) {
|
|||
}
|
||||
|
||||
export function TrackListItem({track, privateFeed = false}) {
|
||||
return <Item key={track.slug}>
|
||||
<Item.Image size="tiny" src={track.author.image} />
|
||||
<Item.Content>
|
||||
<Item.Header as={Link} to={`/tracks/${track.slug}`}>{track.title}</Item.Header>
|
||||
<Item.Meta>
|
||||
Created by {track.author.username} on {track.createdAt}
|
||||
</Item.Meta>
|
||||
<Item.Description>{track.description}</Item.Description>
|
||||
{privateFeed && <Item.Extra>
|
||||
{track.visible ? (
|
||||
<>
|
||||
<Icon color="blue" name="eye" fitted /> Public
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="eye slash" fitted /> Private
|
||||
</>
|
||||
)}
|
||||
</Item.Extra>}
|
||||
</Item.Content>
|
||||
</Item>
|
||||
return (
|
||||
<Item key={track.slug}>
|
||||
<Item.Image size="tiny" src={track.author.image} />
|
||||
<Item.Content>
|
||||
<Item.Header as={Link} to={`/tracks/${track.slug}`}>
|
||||
{track.title}
|
||||
</Item.Header>
|
||||
<Item.Meta>
|
||||
Created by {track.author.username} on {track.createdAt}
|
||||
</Item.Meta>
|
||||
<Item.Description>{track.description}</Item.Description>
|
||||
{privateFeed && (
|
||||
<Item.Extra>
|
||||
{track.visible ? (
|
||||
<>
|
||||
<Icon color="blue" name="eye" fitted /> Public
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="eye slash" fitted /> Private
|
||||
</>
|
||||
)}
|
||||
</Item.Extra>
|
||||
)}
|
||||
</Item.Content>
|
||||
</Item>
|
||||
)
|
||||
}
|
||||
|
||||
const TracksPage = connect((state) => ({login: (state as any).login}))(function TracksPage({login, privateFeed}) {
|
||||
|
|
56
src/types.ts
56
src/types.ts
|
@ -1,45 +1,43 @@
|
|||
export type UserProfile = {
|
||||
username: string,
|
||||
image: string,
|
||||
bio?: string|null,
|
||||
username: string
|
||||
image: string
|
||||
bio?: string | null
|
||||
}
|
||||
|
||||
export type Track = {
|
||||
slug: string,
|
||||
author: UserProfile,
|
||||
title: string,
|
||||
description?: string,
|
||||
createdAt: string,
|
||||
slug: string
|
||||
author: UserProfile
|
||||
title: string
|
||||
description?: string
|
||||
createdAt: string
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
|
||||
export type TrackData = {
|
||||
slug: string,
|
||||
numEvents?: number|null,
|
||||
recordedAt?: String|null,
|
||||
recordedUntil?: String|null,
|
||||
trackLength?: number|null,
|
||||
points: TrackPoint[]
|
||||
slug: string
|
||||
numEvents?: number | null
|
||||
recordedAt?: String | null
|
||||
recordedUntil?: String | null
|
||||
trackLength?: number | null
|
||||
points: TrackPoint[]
|
||||
}
|
||||
|
||||
export type TrackPoint = {
|
||||
date: string|null,
|
||||
time: string|null,
|
||||
latitude: number|null,
|
||||
longitude: number|null,
|
||||
course: number|null,
|
||||
speed: number|null,
|
||||
d1: number|null,
|
||||
d2: number|null,
|
||||
flag: number|null,
|
||||
private: number|null,
|
||||
date: string | null
|
||||
time: string | null
|
||||
latitude: number | null
|
||||
longitude: number | null
|
||||
course: number | null
|
||||
speed: number | null
|
||||
d1: number | null
|
||||
d2: number | null
|
||||
flag: number | null
|
||||
private: number | null
|
||||
}
|
||||
|
||||
export type TrackComment = {
|
||||
id: string,
|
||||
body: string,
|
||||
createdAt: string,
|
||||
id: string
|
||||
body: string
|
||||
createdAt: string
|
||||
author: UserProfile
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue