frontend: Settings page

This commit is contained in:
Paul Bienkowski 2021-02-23 21:52:57 +01:00
parent 6297fcd56f
commit d1d7921808
8 changed files with 158 additions and 16 deletions

View file

@ -12240,6 +12240,11 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"react-hook-form": {
"version": "6.15.4",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.4.tgz",
"integrity": "sha512-K+Sw33DtTMengs8OdqFJI3glzNl1wBzSefD/ksQw/hJf9CnOHQAU6qy82eOrh0IRNt2G53sjr7qnnw1JDjvx1w=="
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View file

@ -18,6 +18,7 @@
"proj4": "^2.7.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-hook-form": "^6.15.4",
"react-markdown": "^5.0.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",

View file

@ -1,18 +1,19 @@
import React from 'react'
import {connect} from 'react-redux'
import {Icon, Button} from 'semantic-ui-react'
import {Image, Icon, Button} from 'semantic-ui-react'
import {BrowserRouter as Router, Switch, Route, Link} from 'react-router-dom'
import styles from './App.module.scss'
import {
HomePage,
LoginRedirectPage,
LogoutPage,
NotFoundPage,
TracksPage,
SettingsPage,
TrackPage,
HomePage,
TracksPage,
UploadPage,
LoginRedirectPage,
} from 'pages'
import {LoginButton} from 'components'
@ -39,7 +40,7 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
<Link to="/">Home</Link>
</li>
<li>
<Link to="/feed">Feed</Link>
<Link to="/feed">Tracks</Link>
</li>
<li>
<a href="https://openbikesensor.org/" target="_blank" rel="noreferrer">
@ -49,7 +50,9 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
{login ? (
<>
<li>
<Link to="/settings">Settings</Link>
<Link to="/settings">
<Image src={login.image} avatar />
</Link>
</li>
<li>
<Button as={Link} to="/logout" compact>
@ -60,7 +63,7 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
) : (
<>
<li>
<LoginButton as='a' compact />
<LoginButton as="a" compact />
</li>
</>
)}
@ -89,9 +92,14 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
<LogoutPage />
</Route>
{login && (
<Route path="/upload" exact>
<UploadPage />
</Route>
<>
<Route path="/upload" exact>
<UploadPage />
</Route>
<Route path="/settings" exact>
<SettingsPage />
</Route>
</>
)}
<Route>
<NotFoundPage />

View file

@ -5,6 +5,13 @@ import {setLogin} from 'reducers/login'
import config from 'config.json'
import {create as createPkce} from 'pkce'
class RequestError extends Error {
constructor(message, errors) {
super(message)
this.errors = errors
}
}
class API {
constructor(store) {
this.store = store
@ -182,10 +189,17 @@ class API {
throw new Error('401 Unauthorized')
}
let json
try {
json = await response.json()
} catch (err) {
json = null
}
if (response.status === 200) {
return await response.json()
return json
} else {
return null
throw new RequestError('Error code ' + response.status, json?.errors)
}
}
@ -199,9 +213,9 @@ class API {
}
return await this.fetch(url, {
method: 'post',
...options,
body,
method: 'post',
headers,
})
}
@ -215,6 +229,10 @@ class API {
return await this.get(url, {...options, method: 'delete'})
}
async put(url, options = {}) {
return await this.post(url, {...options, method: 'put'})
}
getAuthFromTokenResponse(tokenResponse) {
return {
tokenType: tokenResponse.token_type,

View file

@ -0,0 +1,108 @@
import React from 'react'
import {connect} from 'react-redux'
import {Message, Icon, Grid, Form, Button, TextArea, Ref, Input} from 'semantic-ui-react'
import {useForm} from 'react-hook-form'
import {setLogin} from 'reducers/login'
import {Page} from 'components'
import api from 'api'
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 {register, handleSubmit} = useForm()
const [loading, setLoading] = React.useState(false)
const [errors, setErrors] = React.useState(null)
const onSave = React.useCallback(
async (changes) => {
setLoading(true)
setErrors(null)
try {
const response = await api.put('/user', {body: {user: changes}})
setLogin(response.user)
} catch (err) {
setErrors(err.errors)
} finally {
setLoading(false)
}
},
[setLoading, setLogin, setErrors]
)
return (
<Page>
<Grid centered relaxed divided>
<Grid.Row>
<Grid.Column width={8}>
<h2>Your profile</h2>
<Message info>All of this information is public.</Message>
<Form onSubmit={handleSubmit(onSave)} loading={loading}>
<Ref innerRef={findInput(register)}>
<Form.Input error={errors?.username} label="Username" name="username" defaultValue={login.username} />
</Ref>
<Form.Field error={errors?.bio}>
<label>Bio</label>
<Ref innerRef={register}>
<TextArea name="bio" rows={4} defaultValue={login.bio} />
</Ref>
</Form.Field>
<Form.Field error={errors?.image}>
<label>Avatar URL</label>
<Ref innerRef={findInput(register)}>
<Input name="image" defaultValue={login.image} />
</Ref>
</Form.Field>
<Button type="submit" primary>
Save
</Button>
</Form>
</Grid.Column>
<Grid.Column width={6}>
<ApiKeyDialog {...{login}} />
</Grid.Column>
</Grid.Row>
</Grid>
</Page>
)
})
const selectField = findInput((ref) => ref?.select())
function ApiKeyDialog({login}) {
const [show, setShow] = React.useState(false)
const onClick = React.useCallback(
(e) => {
e.preventDefault()
setShow(true)
},
[setShow]
)
return (
<>
<h2>Your API Key</h2>
<p>
Here you find your API Key, for use in the OpenBikeSensor. You can
to copy and paste it into your sensor's configuration interface to
allow direct upload from the device.
</p>
<p>Please protect your API Key carefully as it allows full control over your account.</p>
{show ? (
<Ref innerRef={selectField}>
<Input value={login.apiKey} fluid />
</Ref>
) : (
<Button onClick={onClick}><Icon name='lock' /> Show API Key</Button>
)}
</>
)
}
export default SettingsPage

View file

@ -4,6 +4,7 @@ import Markdown from 'react-markdown'
import {FormattedDate} from 'components'
function CommentForm({onSubmit}) {
const [body, setBody] = React.useState('')

View file

@ -16,8 +16,8 @@ function TracksPageTabs() {
const history = useHistory()
const panes = React.useMemo(
() => [
{menuItem: 'Global Feed', url: '/feed'},
{menuItem: 'Your Feed', url: '/feed/my'},
{menuItem: 'Public tracks', url: '/feed'},
{menuItem: 'My tracks', url: '/feed/my'},
],
[]
)

View file

@ -1,7 +1,8 @@
export {default as HomePage} from './HomePage'
export {default as LoginRedirectPage} from './LoginRedirectPage'
export {default as LogoutPage} from './LogoutPage'
export {default as NotFoundPage} from './NotFoundPage'
export {default as LoginRedirectPage} from './LoginRedirectPage'
export {default as SettingsPage} from './SettingsPage'
export {default as TrackPage} from './TrackPage'
export {default as TracksPage} from './TracksPage'
export {default as UploadPage} from './UploadPage'