This commit is contained in:
Paul Bienkowski 2021-02-14 17:20:27 +01:00
parent ec2d5bcf77
commit 66e00359a9
11 changed files with 198 additions and 174 deletions

View file

@ -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 />

View file

@ -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 @@
}
}
}

View file

@ -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>
)
})

View file

@ -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

View file

@ -1,4 +1,4 @@
@import "../../styles.scss";
@import '../../styles.scss';
.page {
@include container;

View file

@ -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;
}

View file

@ -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>
)
}

View file

@ -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="/" />
) : (

View file

@ -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>

View file

@ -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}) {

View file

@ -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
}