frontend: Settings page
This commit is contained in:
parent
6297fcd56f
commit
d1d7921808
8 changed files with 158 additions and 16 deletions
frontend
5
frontend/package-lock.json
generated
5
frontend/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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,
|
||||
|
|
108
frontend/src/pages/SettingsPage.tsx
Normal file
108
frontend/src/pages/SettingsPage.tsx
Normal 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
|
|
@ -4,6 +4,7 @@ import Markdown from 'react-markdown'
|
|||
|
||||
import {FormattedDate} from 'components'
|
||||
|
||||
|
||||
function CommentForm({onSubmit}) {
|
||||
const [body, setBody] = React.useState('')
|
||||
|
||||
|
|
|
@ -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'},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Reference in a new issue