diff --git a/app/src/client.tsx b/app/src/client.tsx index 3840495b..6cc4fab5 100644 --- a/app/src/client.tsx +++ b/app/src/client.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { hydrate } from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; -import App from './frontend/app'; +import { App } from './frontend/app'; const data = (window as any).__PRELOADED_STATE__; // TODO: remove any diff --git a/app/src/frontend/app.test.tsx b/app/src/frontend/app.test.tsx index 4360c7c4..08fcee5c 100644 --- a/app/src/frontend/app.test.tsx +++ b/app/src/frontend/app.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { MemoryRouter } from 'react-router-dom'; -import App from './app'; +import { App } from './app'; describe('', () => { test('renders without exploding', () => { diff --git a/app/src/frontend/app.tsx b/app/src/frontend/app.tsx index 890d975e..62e09626 100644 --- a/app/src/frontend/app.tsx +++ b/app/src/frontend/app.tsx @@ -1,9 +1,11 @@ -import React, { Fragment } from 'react'; -import { Link, Route, Switch } from 'react-router-dom'; +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; import 'bootstrap/dist/css/bootstrap.min.css'; import './app.css'; +import { AuthRoute, PrivateRoute } from './route'; +import { AuthContext, AuthProvider } from './auth-context'; import { Header } from './header'; import MapApp from './map-app'; import { Building } from './models/building'; @@ -20,10 +22,11 @@ import OrdnanceSurveyLicencePage from './pages/ordnance-survey-licence'; import OrdnanceSurveyUprnPage from './pages/ordnance-survey-uprn'; import PrivacyPolicyPage from './pages/privacy-policy'; import ForgottenPassword from './user/forgotten-password'; -import Login from './user/login'; -import MyAccountPage from './user/my-account'; +import { Login } from './user/login'; +import { MyAccountPage } from './user/my-account'; import PasswordReset from './user/password-reset'; -import SignUp from './user/signup'; +import { SignUp } from './user/signup'; +import { NotFound } from './pages/not-found'; interface AppProps { @@ -34,10 +37,6 @@ interface AppProps { revisionId: number; } -interface AppState { - user?: User; -} - /** * App component * @@ -50,64 +49,27 @@ interface AppState { * map or other pages are rendered, based on the URL. Use a react-router-dom in * child components to navigate without a full page reload. */ -class App extends React.Component { - static mapAppPaths = ['/', '/:mode(view|edit|multi-edit)/:category/:building(\\d+)?/(history)?']; - constructor(props: Readonly) { - super(props); +export const App: React.FC = props => { + const mapAppPaths = ['/', '/:mode(view|edit|multi-edit)/:category/:building(\\d+)?/(history)?']; - this.state = { - user: props.user - }; - this.login = this.login.bind(this); - this.updateUser = this.updateUser.bind(this); - this.logout = this.logout.bind(this); - } - - login(user) { - if (user.error) { - this.logout(); - return; - } - this.setState({user: user}); - } - - updateUser(user){ - this.setState({user: { ...this.state.user, ...user }}); - } - - logout() { - this.setState({user: undefined}); - } - - render() { - return ( - + return ( + - -
+ +
-
+
- - - - - - - - - - - + + + + + @@ -118,38 +80,22 @@ class App extends React.Component { - ( - + ( + + {({user}) => + + } + )} /> - - ); - } -} - -/** - * Component to fall back on in case of 404 or no other match - */ -const NotFound = () => ( -
-
-

Page not found

-

- - We can’t find that one anywhere. - -

- Back home -
-
-); - -export default App; + + ); +}; diff --git a/app/src/frontend/auth-context.tsx b/app/src/frontend/auth-context.tsx new file mode 100644 index 00000000..c21729c0 --- /dev/null +++ b/app/src/frontend/auth-context.tsx @@ -0,0 +1,203 @@ +import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; + +import { apiPost, apiGet, apiDelete } from './apiHelpers'; +import { User } from './models/user'; + +interface AuthContextState { + isAuthenticated: boolean; + isLoading: boolean; + user: User; + userError: string; + login: (data: UserLoginData, cb?: (err) => void) => void; + logout: (cb?: (err) => void) => void; + signup: (data: UserSignupData, cb?: (err) => void) => void; + deleteAccount: (cb?: (err) => void) => void; + generateApiKey: (cb?: (err) => void) => void; +} + +interface UserLoginData { + username: string; + password: string; +} + +interface UserSignupData { + username: string; + email: string; + confirmEmail: string; + password: string; +} + +const stub = (): never => { + throw new Error('AuthProvider not set up'); +}; + +export const AuthContext = createContext({ + isAuthenticated: false, + isLoading: false, + user: undefined, + userError: undefined, + login: stub, + logout: stub, + signup: stub, + generateApiKey: stub, + deleteAccount: stub, +}); + +const noop = () => {}; + +export const AuthProvider: React.FC = ({ + children +}) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [user, setUser] = useState(undefined); + const [userError, setUserError] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + + const login = useCallback(async (data: UserLoginData, cb: (err) => void = noop) => { + if(isAuthenticated) { + return; + } + + setIsLoading(true); + + try { + const res = await apiPost('/api/login', { ...data }); + + if (res.error) { + setIsLoading(false); + cb(res.error); + } else { + setIsAuthenticated(true); + } + } catch(err) { + cb('Error logging in.'); + } + }, [isAuthenticated]); + + const logout = useCallback(async (cb: (err) => void = noop) => { + if(!isAuthenticated) { + return; + } + + setIsLoading(true); + + try { + const res = await apiPost('/api/logout'); + + if (res.error) { + setIsLoading(false); + cb(res.error); + } else { + setIsAuthenticated(false); + } + } catch(err) { + cb('Error logging out'); + } + + }, [isAuthenticated]); + + const signup = useCallback(async (data: UserSignupData, cb: (err) => void = noop) => { + if(isAuthenticated) { + return; + } + + setIsLoading(true); + + try { + const res = await apiPost('/api/users', { + username: data.username, + email: data.email, + confirm_email: data.confirmEmail, + password: data.password + }); + + if(res.error) { + setIsLoading(false); + cb(res.error); + } else { + setIsAuthenticated(true); + } + } catch(err) { + cb('Error signing up.'); + } + }, [isAuthenticated]); + + async function updateUserData(): Promise { + setUserError(undefined); + setIsLoading(true); + + try { + const user = await apiGet('/api/users/me'); + if (user.error) { + setUserError(user.error); + } else { + setUser(user); + setIsAuthenticated(true); + } + } catch(err) { + setUserError('Error loading user info.'); + } + + setIsLoading(false); + } + + const generateApiKey = useCallback(async (cb: (err) => void = noop) => { + try { + const res = await apiPost('/api/api/key'); + if (res.error) { + cb(res.error); + } else { + updateUserData(); + } + } catch(err) { + cb('Error getting API key.'); + } + }, []); + + const deleteAccount = useCallback(async (cb) => { + try { + const data = await apiDelete('/api/users/me'); + if (data.error) { + cb(data.error); + } else { + setIsAuthenticated(false); + } + } catch(err) { + cb('Error getting API key.'); + } + }, []); + + useEffect(() => { + if(isAuthenticated) { + updateUserData(); + } else { + setUser(undefined); + setIsLoading(false); + } + }, [isAuthenticated]); + + // update user data initially to check if already logged in + useEffect(() => { + updateUserData(); + }, []); + + return ( + + {children} + + ); +}; + +export const useAuth = (): AuthContextState => { + return useContext(AuthContext); +}; diff --git a/app/src/frontend/building/data-container.tsx b/app/src/frontend/building/data-container.tsx index 8370c346..1462c615 100644 --- a/app/src/frontend/building/data-container.tsx +++ b/app/src/frontend/building/data-container.tsx @@ -235,10 +235,6 @@ const withCopyEdit = (WrappedComponent: React.ComponentType) } render() { - if (this.props.mode === 'edit' && !this.props.user){ - return ; - } - const currentBuilding = this.getEditedBuilding(); const values_to_copy = {}; diff --git a/app/src/frontend/building/multi-edit.tsx b/app/src/frontend/building/multi-edit.tsx index ecc39e98..9cc2dc94 100644 --- a/app/src/frontend/building/multi-edit.tsx +++ b/app/src/frontend/building/multi-edit.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Link, Redirect } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { parseJsonOrDefault } from '../../helpers'; import ErrorBox from '../components/error-box'; @@ -18,9 +18,6 @@ interface MultiEditProps { } const MultiEdit: React.FC = (props) => { - if (!props.user){ - return ; - } if (props.category === 'like') { // special case for likes return ( diff --git a/app/src/frontend/components/icons.tsx b/app/src/frontend/components/icons.tsx index cde73bb2..2e6709af 100644 --- a/app/src/frontend/components/icons.tsx +++ b/app/src/frontend/components/icons.tsx @@ -16,6 +16,7 @@ import { faPaintBrush, faQuestionCircle, faSearch, + faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -35,7 +36,8 @@ library.add( faCaretUp, faCaretRight, faSearch, - faEye + faEye, + faSpinner ); const HelpIcon = () => ( @@ -94,6 +96,10 @@ const SearchIcon = () => ( ); +const SpinnerIcon: React.FC<{spin?: boolean}> = ({spin=true}) => ( + +); + export { HelpIcon, InfoIcon, @@ -108,5 +114,6 @@ export { UpIcon, RightIcon, SearchIcon, - VerifyIcon + VerifyIcon, + SpinnerIcon }; diff --git a/app/src/frontend/header.tsx b/app/src/frontend/header.tsx index d07f2ef7..f2569ba6 100644 --- a/app/src/frontend/header.tsx +++ b/app/src/frontend/header.tsx @@ -5,7 +5,7 @@ import './header.css'; import { Logo } from './components/logo'; import { WithSeparator } from './components/with-separator'; -import { User } from './models/user'; +import { useAuth } from './auth-context'; interface MenuLink { @@ -132,37 +132,38 @@ function getCurrentMenuLinks(username: string): MenuLink[][] { ]; } +const Menu: React.FC<{ onNavigate: () => void }> = ({ onNavigate }) => { + const { user } = useAuth(); -const Menu : React.FC<{ - menuLinkSections: MenuLink[][], - onNavigate: () => void -}> = ({ menuLinkSections, onNavigate }) => ( - }> - {menuLinkSections.map((section, idx) => -
    - {section.map(item => ( -
  • - { - item.disabled ? - {item.text} : - item.external ? - {item.text} : - {item.text} - } -
  • - ))} -
- )} -
-); + const menuLinkSections = getCurrentMenuLinks(user?.username); + return ( + }> + {menuLinkSections.map((section, idx) => +
    + {section.map(item => ( +
  • + { + item.disabled ? + {item.text} : + item.external ? + {item.text} : + {item.text} + } +
  • + ))} +
+ )} +
+ ); +}; -const InternalNavLink: React.FC<{to: string, onClick: () => void}> = ({ to, onClick, children}) => ( +const InternalNavLink: React.FC<{to: string; onClick: () => void}> = ({ to, onClick, children}) => ( {children} ); -const ExternalNavLink: React.FC<{to:string}> = ({ to, children }) => ( +const ExternalNavLink: React.FC<{to: string}> = ({ to, children }) => ( {children} @@ -175,17 +176,14 @@ const LinkStub: React.FC<{note: string}> = ({note, children}) => ( ); -export const Header : React.FC<{ - user: User; +export const Header: React.FC<{ animateLogo: boolean; -}> = ({ user, animateLogo }) => { +}> = ({ animateLogo }) => { const [collapseMenu, setCollapseMenu] = useState(true); const toggleCollapse = () => setCollapseMenu(!collapseMenu); const handleNavigate = () => setCollapseMenu(true); - const currentMenuLinks = getCurrentMenuLinks(user?.username); - return (
@@ -203,7 +201,7 @@ export const Header : React.FC<{
); diff --git a/app/src/frontend/map-app.tsx b/app/src/frontend/map-app.tsx index b17f516f..7c7df896 100644 --- a/app/src/frontend/map-app.tsx +++ b/app/src/frontend/map-app.tsx @@ -14,6 +14,7 @@ import Sidebar from './building/sidebar'; import ColouringMap from './map/map'; import { Building } from './models/building'; import Welcome from './pages/welcome'; +import { PrivateRoute } from './route'; interface MapAppRouteParams { mode: 'view' | 'edit' | 'multi-edit'; @@ -205,6 +206,9 @@ class MapApp extends React.Component { return ( + + {/* empty private route to ensure auth for editing */} + diff --git a/app/src/frontend/pages/not-found.tsx b/app/src/frontend/pages/not-found.tsx new file mode 100644 index 00000000..fea7896f --- /dev/null +++ b/app/src/frontend/pages/not-found.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +/** + * Component to fall back on in case of 404 or no other match + */ +export const NotFound: React.FC = () => ( +
+
+

Page not found

+

+ + We can’t find that one anywhere. + +

+ Back home +
+
+); diff --git a/app/src/frontend/route.tsx b/app/src/frontend/route.tsx new file mode 100644 index 00000000..47db3003 --- /dev/null +++ b/app/src/frontend/route.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import { Route, RouteProps, Redirect } from 'react-router-dom'; + +import { useAuth } from './auth-context'; + +export const PrivateRoute: React.FC = ({component: Component, children, ...rest}) => { + const { isAuthenticated } = useAuth(); + + return { + if(isAuthenticated) { + if(Component) { + return ; + } else if(children) { + return <>{children}; + } + + } else { + return ; + } + }} + /> +}; + +export const AuthRoute: React.FC = ({ component: Component, children, ...rest}) => { + const { isAuthenticated } = useAuth(); + + return { + if(isAuthenticated) { + const { from } = props.location.state ?? { from: '/my-account.html'}; + return ; + } else { + if(Component) { + return ; + } else if(children) { + return <>{children}; + } + } + }} + /> +}; diff --git a/app/src/frontend/user/login.tsx b/app/src/frontend/user/login.tsx index 497e1b4a..2d189967 100644 --- a/app/src/frontend/user/login.tsx +++ b/app/src/frontend/user/login.tsx @@ -1,127 +1,83 @@ -import React, { Component } from 'react'; -import { Link, Redirect } from 'react-router-dom'; +import React, { useCallback, useState } from 'react'; +import { Link } from 'react-router-dom'; -import { apiGet, apiPost } from '../apiHelpers'; +import { useAuth } from '../auth-context'; import ErrorBox from '../components/error-box'; +import { SpinnerIcon } from '../components/icons'; import InfoBox from '../components/info-box'; import SupporterLogos from '../components/supporter-logos'; -import { User } from '../models/user'; -interface LoginProps { - user: User; - login: (user: User) => void; -} +export const Login: React.FC = () => { + const {isLoading, login } = useAuth(); -class Login extends Component { - constructor(props) { - super(props); - this.state = { - username: '', - password: '', - show_password: '', - error: undefined - }; + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - } + const [error, setError] = useState(undefined); - handleChange(event) { - const target = event.target; - const value = target.type === 'checkbox' ? target.checked : target.value; - const name = target.name; + const onSubmit = useCallback((e) => { + e.preventDefault(); + setError(undefined); - this.setState({ - [name]: value - }); - } + login({ username, password }); + }, [username, password]); - handleSubmit(event) { - event.preventDefault(); - this.setState({error: undefined}); + return ( +
+
+

Log in

+ +
Please discuss + suggestions for improvements and + report issues or problems. +
+ +
+ + setUsername(e.target.value)} + placeholder="not-your-real-name" required + /> - apiPost('/api/login', this.state) - .then(res => { - if (res.error) { - this.setState({error: res.error}); - } else { - apiGet('/api/users/me') - .then(user => { - if (user.error) { - this.setState({error: user.error}); - } else { - this.props.login(user); - } - }).catch( - (err) => this.setState({error: err}) - ); - } - }).catch( - (err) => this.setState({error: err}) - ); - } + + setPassword(e.target.value)} + required + /> - render() { - if (this.props.user && !this.props.user.error) { - return ; - } - return ( -
-
-

Log in

- -
Please discuss - suggestions for improvements and - report issues or problems. -
- - - - + setShowPassword(e.target.checked)} /> + + - - + Forgotten password? -
- - -
+
+ + {isLoading && Logging in...} +
- Forgotten password? + Would you like to create an account instead? -
- -
+
+ Sign Up +
+ +
+
+
+ +
+
+ ) - Would you like to create an account instead? - -
- Sign Up -
- - -
-
-
- -
-
- ); - } -} - -export default Login; +}; diff --git a/app/src/frontend/user/my-account.tsx b/app/src/frontend/user/my-account.tsx index a473ff85..f1bc91a1 100644 --- a/app/src/frontend/user/my-account.tsx +++ b/app/src/frontend/user/my-account.tsx @@ -1,173 +1,115 @@ -import React, { Component, FormEvent } from 'react'; -import { Link, Redirect } from 'react-router-dom'; +import React, { useCallback, useState } from 'react'; +import { Link } from 'react-router-dom'; -import { apiDelete, apiPost } from '../apiHelpers'; +import { useAuth } from '../auth-context'; import ConfirmationModal from '../components/confirmation-modal'; import ErrorBox from '../components/error-box'; -import { User } from '../models/user'; +import { SpinnerIcon } from '../components/icons'; -interface MyAccountPageProps { - user: User; - updateUser: (user: User) => void; - logout: () => void; -} +export const MyAccountPage: React.FC = () => { + const { isLoading, user, userError, logout, generateApiKey, deleteAccount } = useAuth(); -interface MyAccountPageState { - showDeleteConfirm: boolean; - error: string; -} + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [error, setError] = useState(undefined); + const handleLogout = useCallback((e) => { + e.preventDefault(); + logout(setError); + }, [logout]); -class MyAccountPage extends Component { - constructor(props) { - super(props); - this.state = { - error: undefined, - showDeleteConfirm: false - }; - this.handleLogout = this.handleLogout.bind(this); - this.handleGenerateKey = this.handleGenerateKey.bind(this); - } + const handleGenerateKey = useCallback(async (e) => { + e.preventDefault(); + + setError(undefined); + generateApiKey(setError); + }, [generateApiKey]); - handleLogout(event) { - event.preventDefault(); - this.setState({error: undefined}); + const handleDeleteAccount = useCallback(() => { + setError(undefined); + deleteAccount(setError); + }, [deleteAccount]) - apiPost('/api/logout') - .then(function(res){ - if (res.error) { - this.setState({error: res.error}); - } else { - this.props.logout(); - } - }.bind(this)).catch( - (err) => this.setState({error: err}) + if(!user && isLoading) { + return ( +
+
+ Loading user info... +
+
); } - handleGenerateKey(event) { - event.preventDefault(); - this.setState({error: undefined}); + return ( +
+
+ { !isLoading && } + {!userError && (<> +

Welcome, {user.username}!

+

+ Colouring London is under active development. Please + discuss suggestions for improvements and + report issues or problems. +

+

+ For reference, here are the + privacy policy, + contributor agreement and + data accuracy agreement. +

+ +
+
+ Start colouring + +
+
- apiPost('/api/api/key') - .then(res => { - if (res.error) { - this.setState({error: res.error}); - } else { - this.props.updateUser(res); - } - }).catch( - (err) => this.setState({error: err}) - ); - } +
+

My Details

+

Username

+

{user.username}

+

Email Address

+

{user.email || '-'}

+

Registered

+

{user.registered.toString()}

- confirmDelete(event: FormEvent) { - event.preventDefault(); - this.setState({ showDeleteConfirm: true }); - } +
- hideConfirmDelete() { - this.setState({ showDeleteConfirm: false }); - } +

Technical details

+

Are you a software developer? If so, you might be interested in these.

+

API key

+

{user.api_key || '-'}

+
+ +
- async handleDelete() { - this.setState({ error: undefined }); +

Open Source Code

+ Colouring London site code is developed at colouring-london on Github - try { - const data = await apiDelete('/api/users/me'); +
- if(data.error) { - this.setState({ error: data.error }); - } else { - this.props.logout(); - } - } catch (err) { - this.setState({ error: err }); - } finally { - this.hideConfirmDelete(); - } - } +

Account actions

+
{ + e.preventDefault(); + setShowDeleteConfirm(true); + }} + className="form-group mb-3" + > + +
- render() { - if (this.props.user && !this.props.user.error) { - return ( -
-
-

Welcome, {this.props.user.username}!

-

- - Colouring London is under active development. Please discuss - suggestions for improvements and - report issues or problems. - -

-

- For reference, here are the privacy policy, contributor agreement and data accuracy agreement. -

- -
-
- Start colouring - -
-
- -
- -

My Details

-

Username

-

{this.props.user.username}

-

Email Address

-

{this.props.user.email? this.props.user.email : '-'}

-

Registered

-

{this.props.user.registered.toString()}

- -
- -

Technical details

-

Are you a software developer? If so, you might be interested in these.

-

API key

-

{this.props.user.api_key? this.props.user.api_key : '-'}

-
- -
- -

Open Source Code

- Colouring London site code is developed at colouring-london on Github - -
- -

Account actions

-
this.confirmDelete(e)} - className="form-group mb-3" - > - -
- - this.handleDelete()} - onCancel={() => this.hideConfirmDelete()} - /> - -
-
- ); - } else { - return ( - - ); - } - } -} - -export default MyAccountPage; + handleDeleteAccount()} + onCancel={() => setShowDeleteConfirm(false)} + /> + )} +
+
+ ); +}; diff --git a/app/src/frontend/user/signup.tsx b/app/src/frontend/user/signup.tsx index 9b89d0e8..ac77cc79 100644 --- a/app/src/frontend/user/signup.tsx +++ b/app/src/frontend/user/signup.tsx @@ -1,171 +1,126 @@ -import React, { Component } from 'react'; -import { Link, Redirect } from 'react-router-dom'; +import React, { useCallback, useState } from 'react'; +import { Link } from 'react-router-dom'; -import { apiGet, apiPost } from '../apiHelpers'; +import { useAuth } from '../auth-context'; import ErrorBox from '../components/error-box'; +import { SpinnerIcon } from '../components/icons'; import InfoBox from '../components/info-box'; import SupporterLogos from '../components/supporter-logos'; -import { User } from '../models/user'; -interface SignUpProps { - login: (user: User) => void; - user: User; -} +export const SignUp: React.FC = () => { + const { isLoading, signup } = useAuth(); + const [error, setError] = useState(undefined); -interface SignUpState { - username: string; - email: string; - confirm_email: string; - show_password: boolean; - password: string; - confirm_conditions: boolean; - error: string; -} + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [confirmEmail, setConfirmEmail] = useState('') + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [confirmConditions, setConfirmConditions] = useState(false); -class SignUp extends Component { - constructor(props) { - super(props); - this.state = { - username: '', - email: '', - confirm_email: '', - password: '', - show_password: false, - confirm_conditions: false, - error: undefined - }; + const onSubmit = useCallback( + e => { + e.preventDefault(); + signup({ username, email, confirmEmail, password }, setError); + }, + [username, email, confirmEmail, password, confirmConditions, signup] + ); - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - } + return ( +
+
+

Sign up

+ +
Please discuss + suggestions for improvements and + report issues or problems. +
+

+ Create an account to start colouring in. +

+ +
+ + setUsername(e.target.value)} + placeholder="not-your-real-name" required + minLength={4} + maxLength={30} + pattern="\w+" + title="Usernames can contain only letters, numbers and the underscore" + /> - handleChange(event) { - const target = event.target; - const value = target.type === 'checkbox' ? target.checked : target.value; - const name: keyof SignUpState = target.name; + + setEmail(e.target.value)} + placeholder="someone@example.com" + /> + + + + setConfirmEmail(e.target.value)} + /> - this.setState({ - [name]: value - } as Pick); - } + + setPassword(e.target.value)} + required + minLength={8} + maxLength={128} + /> - async handleSubmit(event) { - event.preventDefault(); - this.setState({error: undefined}); +
+ setShowPassword(e.target.checked)} + /> + +
- try { - const res = await apiPost('/api/users', this.state); - if(res.error) { - this.setState({ error: res.error }); - } else { - const user = await apiGet('/api/users/me'); - this.props.login(user); - } - } catch(err) { - this.setState({error: err}); - } - } +
+ setConfirmConditions(e.target.checked)} + required /> + +
- render() { - if (this.props.user) { - return ; - } - return ( -
-
-

Sign up

- -
Please discuss - suggestions for improvements and - report issues or problems. +
+ + {isLoading && Sending sign up data...} +
+ + Please also read our data ethics policy before using or sharing our data -

- Create an account to start colouring in. -

- - - - - - - - - + Do you already have an account? - - +
+ Log in +
-
- - -
- -
- - -
- -
- -
- - Please also read our data ethics policy before using or sharing our data - - - Do you already have an account? - -
- Log in -
- - -
-
-
- -
-
- ); - } -} - -export default SignUp; + +
+
+
+ +
+
+ ); +}; diff --git a/app/src/frontendRoute.tsx b/app/src/frontendRoute.tsx index e798cb24..90939164 100644 --- a/app/src/frontendRoute.tsx +++ b/app/src/frontendRoute.tsx @@ -12,7 +12,7 @@ import { getUserVerifiedAttributes } from './api/services/building/base'; import { getUserById } from './api/services/user'; -import App from './frontend/app'; +import { App } from './frontend/app'; import { parseBuildingURL } from './parse'; import asyncController from './api/routes/asyncController';