Merge pull request #659 from mz8i/feature/state-mgmt
Move user authentication to context and hooks
This commit is contained in:
commit
bc55871661
@ -6,7 +6,7 @@ import React from 'react';
|
|||||||
import { hydrate } from 'react-dom';
|
import { hydrate } from 'react-dom';
|
||||||
import { BrowserRouter } from 'react-router-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
|
const data = (window as any).__PRELOADED_STATE__; // TODO: remove any
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import App from './app';
|
import { App } from './app';
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
test('renders without exploding', () => {
|
test('renders without exploding', () => {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import { Link, Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
|
||||||
|
import { AuthRoute, PrivateRoute } from './route';
|
||||||
|
import { AuthContext, AuthProvider } from './auth-context';
|
||||||
import { Header } from './header';
|
import { Header } from './header';
|
||||||
import MapApp from './map-app';
|
import MapApp from './map-app';
|
||||||
import { Building } from './models/building';
|
import { Building } from './models/building';
|
||||||
@ -20,10 +22,11 @@ import OrdnanceSurveyLicencePage from './pages/ordnance-survey-licence';
|
|||||||
import OrdnanceSurveyUprnPage from './pages/ordnance-survey-uprn';
|
import OrdnanceSurveyUprnPage from './pages/ordnance-survey-uprn';
|
||||||
import PrivacyPolicyPage from './pages/privacy-policy';
|
import PrivacyPolicyPage from './pages/privacy-policy';
|
||||||
import ForgottenPassword from './user/forgotten-password';
|
import ForgottenPassword from './user/forgotten-password';
|
||||||
import Login from './user/login';
|
import { Login } from './user/login';
|
||||||
import MyAccountPage from './user/my-account';
|
import { MyAccountPage } from './user/my-account';
|
||||||
import PasswordReset from './user/password-reset';
|
import PasswordReset from './user/password-reset';
|
||||||
import SignUp from './user/signup';
|
import { SignUp } from './user/signup';
|
||||||
|
import { NotFound } from './pages/not-found';
|
||||||
|
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
@ -34,10 +37,6 @@ interface AppProps {
|
|||||||
revisionId: number;
|
revisionId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppState {
|
|
||||||
user?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App component
|
* App component
|
||||||
*
|
*
|
||||||
@ -50,64 +49,27 @@ interface AppState {
|
|||||||
* map or other pages are rendered, based on the URL. Use a react-router-dom <Link /> in
|
* map or other pages are rendered, based on the URL. Use a react-router-dom <Link /> in
|
||||||
* child components to navigate without a full page reload.
|
* child components to navigate without a full page reload.
|
||||||
*/
|
*/
|
||||||
class App extends React.Component<AppProps, AppState> {
|
|
||||||
static mapAppPaths = ['/', '/:mode(view|edit|multi-edit)/:category/:building(\\d+)?/(history)?'];
|
|
||||||
|
|
||||||
constructor(props: Readonly<AppProps>) {
|
export const App: React.FC<AppProps> = props => {
|
||||||
super(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 (
|
||||||
<Fragment>
|
<AuthProvider>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path={App.mapAppPaths}>
|
<Route exact path={mapAppPaths}>
|
||||||
<Header user={this.state.user} animateLogo={false} />
|
<Header animateLogo={false} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route>
|
<Route>
|
||||||
<Header user={this.state.user} animateLogo={true} />
|
<Header animateLogo={true} />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/about.html" component={AboutPage} />
|
<Route exact path="/about.html" component={AboutPage} />
|
||||||
<Route exact path="/login.html">
|
<AuthRoute exact path="/login.html" component={Login} />
|
||||||
<Login user={this.state.user} login={this.login} />
|
<AuthRoute exact path="/forgotten-password.html" component={ForgottenPassword} />
|
||||||
</Route>
|
<AuthRoute exact path="/password-reset.html" component={PasswordReset} />
|
||||||
<Route exact path="/forgotten-password.html" component={ForgottenPassword} />
|
<AuthRoute exact path="/sign-up.html" component={SignUp} />
|
||||||
<Route exact path="/password-reset.html" component={PasswordReset} />
|
<PrivateRoute exact path="/my-account.html" component={MyAccountPage} />
|
||||||
<Route exact path="/sign-up.html">
|
|
||||||
<SignUp user={this.state.user} login={this.login} />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/my-account.html">
|
|
||||||
<MyAccountPage
|
|
||||||
user={this.state.user}
|
|
||||||
updateUser={this.updateUser}
|
|
||||||
logout={this.logout}
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
|
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
|
||||||
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
|
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
|
||||||
<Route exact path="/ordnance-survey-licence.html" component={OrdnanceSurveyLicencePage} />
|
<Route exact path="/ordnance-survey-licence.html" component={OrdnanceSurveyLicencePage} />
|
||||||
@ -118,38 +80,22 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
<Route exact path="/code-of-conduct.html" component={CodeOfConductPage} />
|
<Route exact path="/code-of-conduct.html" component={CodeOfConductPage} />
|
||||||
<Route exact path="/leaderboard.html" component={LeaderboardPage} />
|
<Route exact path="/leaderboard.html" component={LeaderboardPage} />
|
||||||
<Route exact path="/history.html" component={ChangesPage} />
|
<Route exact path="/history.html" component={ChangesPage} />
|
||||||
<Route exact path={App.mapAppPaths} render={(props) => (
|
<Route exact path={mapAppPaths} render={(routeProps) => (
|
||||||
|
<AuthContext.Consumer>
|
||||||
|
{({user}) =>
|
||||||
<MapApp
|
<MapApp
|
||||||
{...props}
|
{...routeProps}
|
||||||
building={this.props.building}
|
building={props.building}
|
||||||
building_like={this.props.building_like}
|
building_like={props.building_like}
|
||||||
user_verified={this.props.user_verified}
|
user_verified={props.user_verified}
|
||||||
user={this.state.user}
|
user={user}
|
||||||
revisionId={this.props.revisionId}
|
revisionId={props.revisionId}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
</AuthContext.Consumer>
|
||||||
)} />
|
)} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Fragment>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component to fall back on in case of 404 or no other match
|
|
||||||
*/
|
|
||||||
const NotFound = () => (
|
|
||||||
<article>
|
|
||||||
<section className="main-col">
|
|
||||||
<h1 className="h1">Page not found</h1>
|
|
||||||
<p className="lead">
|
|
||||||
|
|
||||||
We can’t find that one anywhere.
|
|
||||||
|
|
||||||
</p>
|
|
||||||
<Link className="btn btn-outline-dark" to="/">Back home</Link>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
203
app/src/frontend/auth-context.tsx
Normal file
203
app/src/frontend/auth-context.tsx
Normal file
@ -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<AuthContextState>({
|
||||||
|
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<User>(undefined);
|
||||||
|
const [userError, setUserError] = useState<string>(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<void> {
|
||||||
|
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 (
|
||||||
|
<AuthContext.Provider value={{
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading,
|
||||||
|
user,
|
||||||
|
userError,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
signup,
|
||||||
|
generateApiKey,
|
||||||
|
deleteAccount
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuth = (): AuthContextState => {
|
||||||
|
return useContext(AuthContext);
|
||||||
|
};
|
@ -235,10 +235,6 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.props.mode === 'edit' && !this.props.user){
|
|
||||||
return <Redirect to="/sign-up.html" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentBuilding = this.getEditedBuilding();
|
const currentBuilding = this.getEditedBuilding();
|
||||||
|
|
||||||
const values_to_copy = {};
|
const values_to_copy = {};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, Redirect } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { parseJsonOrDefault } from '../../helpers';
|
import { parseJsonOrDefault } from '../../helpers';
|
||||||
import ErrorBox from '../components/error-box';
|
import ErrorBox from '../components/error-box';
|
||||||
@ -18,9 +18,6 @@ interface MultiEditProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MultiEdit: React.FC<MultiEditProps> = (props) => {
|
const MultiEdit: React.FC<MultiEditProps> = (props) => {
|
||||||
if (!props.user){
|
|
||||||
return <Redirect to="/sign-up.html" />;
|
|
||||||
}
|
|
||||||
if (props.category === 'like') {
|
if (props.category === 'like') {
|
||||||
// special case for likes
|
// special case for likes
|
||||||
return (
|
return (
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
faPaintBrush,
|
faPaintBrush,
|
||||||
faQuestionCircle,
|
faQuestionCircle,
|
||||||
faSearch,
|
faSearch,
|
||||||
|
faSpinner,
|
||||||
faTimes
|
faTimes
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
@ -35,7 +36,8 @@ library.add(
|
|||||||
faCaretUp,
|
faCaretUp,
|
||||||
faCaretRight,
|
faCaretRight,
|
||||||
faSearch,
|
faSearch,
|
||||||
faEye
|
faEye,
|
||||||
|
faSpinner
|
||||||
);
|
);
|
||||||
|
|
||||||
const HelpIcon = () => (
|
const HelpIcon = () => (
|
||||||
@ -94,6 +96,10 @@ const SearchIcon = () => (
|
|||||||
<FontAwesomeIcon icon="search" />
|
<FontAwesomeIcon icon="search" />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SpinnerIcon: React.FC<{spin?: boolean}> = ({spin=true}) => (
|
||||||
|
<FontAwesomeIcon icon="spinner" spin={spin} />
|
||||||
|
);
|
||||||
|
|
||||||
export {
|
export {
|
||||||
HelpIcon,
|
HelpIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
@ -108,5 +114,6 @@ export {
|
|||||||
UpIcon,
|
UpIcon,
|
||||||
RightIcon,
|
RightIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
VerifyIcon
|
VerifyIcon,
|
||||||
|
SpinnerIcon
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import './header.css';
|
|||||||
|
|
||||||
import { Logo } from './components/logo';
|
import { Logo } from './components/logo';
|
||||||
import { WithSeparator } from './components/with-separator';
|
import { WithSeparator } from './components/with-separator';
|
||||||
import { User } from './models/user';
|
import { useAuth } from './auth-context';
|
||||||
|
|
||||||
|
|
||||||
interface MenuLink {
|
interface MenuLink {
|
||||||
@ -132,16 +132,16 @@ function getCurrentMenuLinks(username: string): MenuLink[][] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Menu: React.FC<{ onNavigate: () => void }> = ({ onNavigate }) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const Menu : React.FC<{
|
const menuLinkSections = getCurrentMenuLinks(user?.username);
|
||||||
menuLinkSections: MenuLink[][],
|
return (
|
||||||
onNavigate: () => void
|
|
||||||
}> = ({ menuLinkSections, onNavigate }) => (
|
|
||||||
<WithSeparator separator={<hr />}>
|
<WithSeparator separator={<hr />}>
|
||||||
{menuLinkSections.map((section, idx) =>
|
{menuLinkSections.map((section, idx) =>
|
||||||
<ul key={`menu-section-${idx}`} className="navbar-nav flex-container">
|
<ul key={`menu-section-${idx}`} className="navbar-nav flex-container">
|
||||||
{section.map(item => (
|
{section.map(item => (
|
||||||
<li className={`nav-item`}>
|
<li className='nav-item' key={`${item.to}-${item.text}`}>
|
||||||
{
|
{
|
||||||
item.disabled ?
|
item.disabled ?
|
||||||
<LinkStub note={item.note}>{item.text}</LinkStub> :
|
<LinkStub note={item.note}>{item.text}</LinkStub> :
|
||||||
@ -154,15 +154,16 @@ const Menu : React.FC<{
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</WithSeparator>
|
</WithSeparator>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const InternalNavLink: React.FC<{to: string, onClick: () => void}> = ({ to, onClick, children}) => (
|
const InternalNavLink: React.FC<{to: string; onClick: () => void}> = ({ to, onClick, children}) => (
|
||||||
<NavLink className="nav-link" to={to} onClick={onClick}>
|
<NavLink className="nav-link" to={to} onClick={onClick}>
|
||||||
{children}
|
{children}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ExternalNavLink: React.FC<{to:string}> = ({ to, children }) => (
|
const ExternalNavLink: React.FC<{to: string}> = ({ to, children }) => (
|
||||||
<a className="nav-link" href={to}>
|
<a className="nav-link" href={to}>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
@ -175,17 +176,14 @@ const LinkStub: React.FC<{note: string}> = ({note, children}) => (
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Header : React.FC<{
|
export const Header: React.FC<{
|
||||||
user: User;
|
|
||||||
animateLogo: boolean;
|
animateLogo: boolean;
|
||||||
}> = ({ user, animateLogo }) => {
|
}> = ({ animateLogo }) => {
|
||||||
const [collapseMenu, setCollapseMenu] = useState(true);
|
const [collapseMenu, setCollapseMenu] = useState(true);
|
||||||
|
|
||||||
const toggleCollapse = () => setCollapseMenu(!collapseMenu);
|
const toggleCollapse = () => setCollapseMenu(!collapseMenu);
|
||||||
const handleNavigate = () => setCollapseMenu(true);
|
const handleNavigate = () => setCollapseMenu(true);
|
||||||
|
|
||||||
const currentMenuLinks = getCurrentMenuLinks(user?.username);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="main-header navbar navbar-light">
|
<header className="main-header navbar navbar-light">
|
||||||
<div className="nav-header">
|
<div className="nav-header">
|
||||||
@ -203,7 +201,7 @@ export const Header : React.FC<{
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<nav className={collapseMenu ? 'collapse navbar-collapse' : 'navbar-collapse'}>
|
<nav className={collapseMenu ? 'collapse navbar-collapse' : 'navbar-collapse'}>
|
||||||
<Menu menuLinkSections={currentMenuLinks} onNavigate={handleNavigate}></Menu>
|
<Menu onNavigate={handleNavigate}></Menu>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,7 @@ import Sidebar from './building/sidebar';
|
|||||||
import ColouringMap from './map/map';
|
import ColouringMap from './map/map';
|
||||||
import { Building } from './models/building';
|
import { Building } from './models/building';
|
||||||
import Welcome from './pages/welcome';
|
import Welcome from './pages/welcome';
|
||||||
|
import { PrivateRoute } from './route';
|
||||||
|
|
||||||
interface MapAppRouteParams {
|
interface MapAppRouteParams {
|
||||||
mode: 'view' | 'edit' | 'multi-edit';
|
mode: 'view' | 'edit' | 'multi-edit';
|
||||||
@ -205,6 +206,9 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
<Switch>
|
||||||
|
<PrivateRoute path="/:mode(edit|multi-edit)" /> {/* empty private route to ensure auth for editing */}
|
||||||
|
</Switch>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
|
19
app/src/frontend/pages/not-found.tsx
Normal file
19
app/src/frontend/pages/not-found.tsx
Normal file
@ -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 = () => (
|
||||||
|
<article>
|
||||||
|
<section className="main-col">
|
||||||
|
<h1 className="h1">Page not found</h1>
|
||||||
|
<p className="lead">
|
||||||
|
|
||||||
|
We can’t find that one anywhere.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<Link className="btn btn-outline-dark" to="/">Back home</Link>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
);
|
47
app/src/frontend/route.tsx
Normal file
47
app/src/frontend/route.tsx
Normal file
@ -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<RouteProps> = ({component: Component, children, ...rest}) => {
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
|
return <Route
|
||||||
|
{...rest}
|
||||||
|
render={props => {
|
||||||
|
if(isAuthenticated) {
|
||||||
|
if(Component) {
|
||||||
|
return <Component {...props} />;
|
||||||
|
} else if(children) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return <Redirect to={{
|
||||||
|
pathname: "/login.html",
|
||||||
|
state: { from: props.location.pathname }
|
||||||
|
}} />;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthRoute: React.FC<RouteProps> = ({ component: Component, children, ...rest}) => {
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
|
return <Route
|
||||||
|
{...rest}
|
||||||
|
render={props => {
|
||||||
|
if(isAuthenticated) {
|
||||||
|
const { from } = props.location.state ?? { from: '/my-account.html'};
|
||||||
|
return <Redirect to={{pathname: from }} />;
|
||||||
|
} else {
|
||||||
|
if(Component) {
|
||||||
|
return <Component {...props} />;
|
||||||
|
} else if(children) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
};
|
@ -1,70 +1,28 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Link, Redirect } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { apiGet, apiPost } from '../apiHelpers';
|
import { useAuth } from '../auth-context';
|
||||||
import ErrorBox from '../components/error-box';
|
import ErrorBox from '../components/error-box';
|
||||||
|
import { SpinnerIcon } from '../components/icons';
|
||||||
import InfoBox from '../components/info-box';
|
import InfoBox from '../components/info-box';
|
||||||
import SupporterLogos from '../components/supporter-logos';
|
import SupporterLogos from '../components/supporter-logos';
|
||||||
import { User } from '../models/user';
|
|
||||||
|
|
||||||
interface LoginProps {
|
export const Login: React.FC = () => {
|
||||||
user: User;
|
const {isLoading, login } = useAuth();
|
||||||
login: (user: User) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Login extends Component<LoginProps, any> {
|
const [username, setUsername] = useState('');
|
||||||
constructor(props) {
|
const [password, setPassword] = useState('');
|
||||||
super(props);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
this.state = {
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
show_password: '',
|
|
||||||
error: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
const [error, setError] = useState(undefined);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange(event) {
|
const onSubmit = useCallback((e) => {
|
||||||
const target = event.target;
|
e.preventDefault();
|
||||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
setError(undefined);
|
||||||
const name = target.name;
|
|
||||||
|
|
||||||
this.setState({
|
login({ username, password });
|
||||||
[name]: value
|
}, [username, password]);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.setState({error: undefined});
|
|
||||||
|
|
||||||
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})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.props.user && !this.props.user.error) {
|
|
||||||
return <Redirect to="/my-account.html" />;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<article>
|
<article>
|
||||||
<section className="main-col">
|
<section className="main-col">
|
||||||
@ -75,28 +33,28 @@ class Login extends Component<LoginProps, any> {
|
|||||||
href="https://github.com/colouring-london/colouring-london/issues">
|
href="https://github.com/colouring-london/colouring-london/issues">
|
||||||
report issues or problems</a>.
|
report issues or problems</a>.
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
<ErrorBox msg={this.state.error} />
|
<ErrorBox msg={error} />
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<label htmlFor="username">Username*</label>
|
<label htmlFor="username">Username*</label>
|
||||||
<input name="username" id="username"
|
<input name="username" id="username"
|
||||||
className="form-control" type="text"
|
className="form-control" type="text"
|
||||||
value={this.state.username} onChange={this.handleChange}
|
value={username} onChange={e => setUsername(e.target.value)}
|
||||||
placeholder="not-your-real-name" required
|
placeholder="not-your-real-name" required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label htmlFor="password">Password</label>
|
<label htmlFor="password">Password</label>
|
||||||
<input name="password" id="password"
|
<input name="password" id="password"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
type={(this.state.show_password)? 'text': 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={this.state.password} onChange={this.handleChange}
|
value={password} onChange={e => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
<input id="show_password" name="show_password"
|
<input id="show_password" name="show_password"
|
||||||
className="form-check-input" type="checkbox"
|
className="form-check-input" type="checkbox"
|
||||||
checked={this.state.show_password}
|
checked={showPassword}
|
||||||
onChange={this.handleChange}
|
onChange={e => setShowPassword(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="show_password" className="form-check-label">Show password?</label>
|
<label htmlFor="show_password" className="form-check-label">Show password?</label>
|
||||||
</div>
|
</div>
|
||||||
@ -104,7 +62,8 @@ class Login extends Component<LoginProps, any> {
|
|||||||
<Link to="/forgotten-password.html">Forgotten password?</Link>
|
<Link to="/forgotten-password.html">Forgotten password?</Link>
|
||||||
|
|
||||||
<div className="buttons-container with-space">
|
<div className="buttons-container with-space">
|
||||||
<input type="submit" value="Log In" className="btn btn-primary" />
|
<input type="submit" disabled={isLoading} value="Log In" className="btn btn-primary" />
|
||||||
|
{isLoading && <span><SpinnerIcon />Logging in...</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Would you like to create an account instead?
|
Would you like to create an account instead?
|
||||||
@ -112,7 +71,6 @@ class Login extends Component<LoginProps, any> {
|
|||||||
<div className="buttons-container with-space">
|
<div className="buttons-container with-space">
|
||||||
<Link to="sign-up.html" className="btn btn-outline-dark">Sign Up</Link>
|
<Link to="sign-up.html" className="btn btn-outline-dark">Sign Up</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<hr />
|
<hr />
|
||||||
@ -120,8 +78,6 @@ class Login extends Component<LoginProps, any> {
|
|||||||
<SupporterLogos />
|
<SupporterLogos />
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
);
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Login;
|
};
|
||||||
|
@ -1,115 +1,63 @@
|
|||||||
import React, { Component, FormEvent } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Link, Redirect } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { apiDelete, apiPost } from '../apiHelpers';
|
import { useAuth } from '../auth-context';
|
||||||
import ConfirmationModal from '../components/confirmation-modal';
|
import ConfirmationModal from '../components/confirmation-modal';
|
||||||
import ErrorBox from '../components/error-box';
|
import ErrorBox from '../components/error-box';
|
||||||
import { User } from '../models/user';
|
import { SpinnerIcon } from '../components/icons';
|
||||||
|
|
||||||
interface MyAccountPageProps {
|
export const MyAccountPage: React.FC = () => {
|
||||||
user: User;
|
const { isLoading, user, userError, logout, generateApiKey, deleteAccount } = useAuth();
|
||||||
updateUser: (user: User) => void;
|
|
||||||
logout: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MyAccountPageState {
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
showDeleteConfirm: boolean;
|
const [error, setError] = useState(undefined);
|
||||||
error: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const handleLogout = useCallback((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
logout(setError);
|
||||||
|
}, [logout]);
|
||||||
|
|
||||||
class MyAccountPage extends Component<MyAccountPageProps, MyAccountPageState> {
|
const handleGenerateKey = useCallback(async (e) => {
|
||||||
constructor(props) {
|
e.preventDefault();
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
error: undefined,
|
|
||||||
showDeleteConfirm: false
|
|
||||||
};
|
|
||||||
this.handleLogout = this.handleLogout.bind(this);
|
|
||||||
this.handleGenerateKey = this.handleGenerateKey.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLogout(event) {
|
setError(undefined);
|
||||||
event.preventDefault();
|
generateApiKey(setError);
|
||||||
this.setState({error: undefined});
|
}, [generateApiKey]);
|
||||||
|
|
||||||
apiPost('/api/logout')
|
const handleDeleteAccount = useCallback(() => {
|
||||||
.then(function(res){
|
setError(undefined);
|
||||||
if (res.error) {
|
deleteAccount(setError);
|
||||||
this.setState({error: res.error});
|
}, [deleteAccount])
|
||||||
} else {
|
|
||||||
this.props.logout();
|
|
||||||
}
|
|
||||||
}.bind(this)).catch(
|
|
||||||
(err) => this.setState({error: err})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGenerateKey(event) {
|
if(!user && isLoading) {
|
||||||
event.preventDefault();
|
|
||||||
this.setState({error: undefined});
|
|
||||||
|
|
||||||
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})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmDelete(event: FormEvent<HTMLFormElement>) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.setState({ showDeleteConfirm: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
hideConfirmDelete() {
|
|
||||||
this.setState({ showDeleteConfirm: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleDelete() {
|
|
||||||
this.setState({ error: undefined });
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.props.user && !this.props.user.error) {
|
|
||||||
return (
|
return (
|
||||||
<article>
|
<article>
|
||||||
<section className="main-col">
|
<section className="main-col">
|
||||||
<h1 className="h1">Welcome, {this.props.user.username}!</h1>
|
<SpinnerIcon spin={true} /> Loading user info...
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article>
|
||||||
|
<section className="main-col">
|
||||||
|
{ !isLoading && <ErrorBox msg={userError} /> }
|
||||||
|
{!userError && (<>
|
||||||
|
<h1 className="h1">Welcome, {user.username}!</h1>
|
||||||
<p>
|
<p>
|
||||||
|
Colouring London is under active development. Please
|
||||||
Colouring London is under active development. Please <a href="https://discuss.colouring.london/">discuss
|
<a href="https://discuss.colouring.london/">discuss suggestions for improvements</a> and
|
||||||
suggestions for improvements</a> and <a
|
<a href="https://github.com/colouring-london/colouring-london/issues"> report issues or problems</a>.
|
||||||
href="https://github.com/colouring-london/colouring-london/issues">
|
|
||||||
report issues or problems</a>.
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
For reference, here are the <Link
|
For reference, here are the
|
||||||
to="/privacy-policy.html">privacy policy</Link>, <Link
|
<Link to="/privacy-policy.html">privacy policy</Link>,
|
||||||
to="/contributor-agreement.html">contributor agreement</Link> and <Link
|
<Link to="/contributor-agreement.html">contributor agreement</Link> and
|
||||||
to="/data-accuracy.html">data accuracy agreement</Link>.
|
<Link to="/data-accuracy.html">data accuracy agreement</Link>.
|
||||||
</p>
|
</p>
|
||||||
<ErrorBox msg={this.state.error} />
|
<ErrorBox msg={error} />
|
||||||
<form onSubmit={this.handleLogout}>
|
<form onSubmit={handleLogout}>
|
||||||
<div className="buttons-container">
|
<div className="buttons-container">
|
||||||
<Link to="/edit/age" className="btn btn-warning">Start colouring</Link>
|
<Link to="/edit/age" className="btn btn-warning">Start colouring</Link>
|
||||||
<input className="btn btn-secondary" type="submit" value="Log out"/>
|
<input className="btn btn-secondary" type="submit" value="Log out"/>
|
||||||
@ -117,22 +65,21 @@ class MyAccountPage extends Component<MyAccountPageProps, MyAccountPageState> {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
<h2 className="h2">My Details</h2>
|
<h2 className="h2">My Details</h2>
|
||||||
<h3 className="h3">Username</h3>
|
<h3 className="h3">Username</h3>
|
||||||
<p>{this.props.user.username}</p>
|
<p>{user.username}</p>
|
||||||
<h3 className="h3">Email Address</h3>
|
<h3 className="h3">Email Address</h3>
|
||||||
<p>{this.props.user.email? this.props.user.email : '-'}</p>
|
<p>{user.email || '-'}</p>
|
||||||
<h3 className="h3">Registered</h3>
|
<h3 className="h3">Registered</h3>
|
||||||
<p>{this.props.user.registered.toString()}</p>
|
<p>{user.registered.toString()}</p>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
<h2 className="h2">Technical details</h2>
|
<h2 className="h2">Technical details</h2>
|
||||||
<p>Are you a software developer? If so, you might be interested in these.</p>
|
<p>Are you a software developer? If so, you might be interested in these.</p>
|
||||||
<h3 className="h3">API key</h3>
|
<h3 className="h3">API key</h3>
|
||||||
<p>{this.props.user.api_key? this.props.user.api_key : '-'}</p>
|
<p>{user.api_key || '-'}</p>
|
||||||
<form onSubmit={this.handleGenerateKey} className="form-group mb-3">
|
<form onSubmit={handleGenerateKey} className="form-group mb-3">
|
||||||
<input className="btn btn-warning" type="submit" value="Generate API key"/>
|
<input className="btn btn-warning" type="submit" value="Generate API key"/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -143,31 +90,26 @@ class MyAccountPage extends Component<MyAccountPageProps, MyAccountPageState> {
|
|||||||
|
|
||||||
<h2 className="h2">Account actions</h2>
|
<h2 className="h2">Account actions</h2>
|
||||||
<form
|
<form
|
||||||
onSubmit={e => this.confirmDelete(e)}
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
|
}}
|
||||||
className="form-group mb-3"
|
className="form-group mb-3"
|
||||||
>
|
>
|
||||||
<input className="btn btn-danger" type="submit" value="Delete account" />
|
<input className="btn btn-danger" type="submit" value="Delete account" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
show={this.state.showDeleteConfirm}
|
show={showDeleteConfirm}
|
||||||
title="Confirm account deletion"
|
title="Confirm account deletion"
|
||||||
description="Are you sure you want to delete your account? This cannot be undone."
|
description="Are you sure you want to delete your account? This cannot be undone."
|
||||||
confirmButtonText="Delete account"
|
confirmButtonText="Delete account"
|
||||||
confirmButtonClass="btn-danger"
|
confirmButtonClass="btn-danger"
|
||||||
onConfirm={() => this.handleDelete()}
|
onConfirm={() => handleDeleteAccount()}
|
||||||
onCancel={() => this.hideConfirmDelete()}
|
onCancel={() => setShowDeleteConfirm(false)}
|
||||||
/>
|
/>
|
||||||
|
</>)}
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
} else {
|
};
|
||||||
return (
|
|
||||||
<Redirect to="/login.html" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MyAccountPage;
|
|
||||||
|
@ -1,75 +1,31 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Link, Redirect } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { apiGet, apiPost } from '../apiHelpers';
|
import { useAuth } from '../auth-context';
|
||||||
import ErrorBox from '../components/error-box';
|
import ErrorBox from '../components/error-box';
|
||||||
|
import { SpinnerIcon } from '../components/icons';
|
||||||
import InfoBox from '../components/info-box';
|
import InfoBox from '../components/info-box';
|
||||||
import SupporterLogos from '../components/supporter-logos';
|
import SupporterLogos from '../components/supporter-logos';
|
||||||
import { User } from '../models/user';
|
|
||||||
|
|
||||||
interface SignUpProps {
|
export const SignUp: React.FC = () => {
|
||||||
login: (user: User) => void;
|
const { isLoading, signup } = useAuth();
|
||||||
user: User;
|
const [error, setError] = useState(undefined);
|
||||||
}
|
|
||||||
|
|
||||||
interface SignUpState {
|
const [username, setUsername] = useState('');
|
||||||
username: string;
|
const [email, setEmail] = useState('');
|
||||||
email: string;
|
const [confirmEmail, setConfirmEmail] = useState('')
|
||||||
confirm_email: string;
|
const [password, setPassword] = useState('');
|
||||||
show_password: boolean;
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
password: string;
|
const [confirmConditions, setConfirmConditions] = useState(false);
|
||||||
confirm_conditions: boolean;
|
|
||||||
error: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SignUp extends Component<SignUpProps, SignUpState> {
|
const onSubmit = useCallback(
|
||||||
constructor(props) {
|
e => {
|
||||||
super(props);
|
e.preventDefault();
|
||||||
this.state = {
|
signup({ username, email, confirmEmail, password }, setError);
|
||||||
username: '',
|
},
|
||||||
email: '',
|
[username, email, confirmEmail, password, confirmConditions, signup]
|
||||||
confirm_email: '',
|
);
|
||||||
password: '',
|
|
||||||
show_password: false,
|
|
||||||
confirm_conditions: false,
|
|
||||||
error: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange(event) {
|
|
||||||
const target = event.target;
|
|
||||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
|
||||||
const name: keyof SignUpState = target.name;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
[name]: value
|
|
||||||
} as Pick<SignUpState, keyof SignUpState>);
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleSubmit(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.setState({error: undefined});
|
|
||||||
|
|
||||||
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});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.props.user) {
|
|
||||||
return <Redirect to="/my-account.html" />;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<article>
|
<article>
|
||||||
<section className="main-col">
|
<section className="main-col">
|
||||||
@ -83,12 +39,12 @@ class SignUp extends Component<SignUpProps, SignUpState> {
|
|||||||
<p>
|
<p>
|
||||||
Create an account to start colouring in.
|
Create an account to start colouring in.
|
||||||
</p>
|
</p>
|
||||||
<ErrorBox msg={this.state.error} />
|
<ErrorBox msg={error} />
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<label htmlFor="username">Username*</label>
|
<label htmlFor="username">Username*</label>
|
||||||
<input name="username" id="username"
|
<input name="username" id="username"
|
||||||
className="form-control" type="text"
|
className="form-control" type="text"
|
||||||
value={this.state.username} onChange={this.handleChange}
|
value={username} onChange={e => setUsername(e.target.value)}
|
||||||
placeholder="not-your-real-name" required
|
placeholder="not-your-real-name" required
|
||||||
minLength={4}
|
minLength={4}
|
||||||
maxLength={30}
|
maxLength={30}
|
||||||
@ -99,21 +55,22 @@ class SignUp extends Component<SignUpProps, SignUpState> {
|
|||||||
<label htmlFor="email">Email (optional)</label>
|
<label htmlFor="email">Email (optional)</label>
|
||||||
<input name="email" id="email"
|
<input name="email" id="email"
|
||||||
className="form-control" type="email"
|
className="form-control" type="email"
|
||||||
value={this.state.email} onChange={this.handleChange}
|
value={email} onChange={e => setEmail(e.target.value)}
|
||||||
placeholder="someone@example.com"
|
placeholder="someone@example.com"
|
||||||
/>
|
/>
|
||||||
<InfoBox msg="Please note that if you forget your password, you will only be able to recover your account if you provide an email address." />
|
<InfoBox msg="Please note that if you forget your password, you will only be able to recover your account if you provide an email address." />
|
||||||
|
|
||||||
<label htmlFor="confirm_email">Confirm email (optional)</label>
|
<label htmlFor="confirm_email">Confirm email (optional)</label>
|
||||||
<input name="confirm_email" id="confirm_email"
|
<input name="confirm_email" id="confirm_email"
|
||||||
className="form-control" type="email"
|
className="form-control" type="email"
|
||||||
value={this.state.confirm_email} onChange={this.handleChange}
|
value={confirmEmail} onChange={e => setConfirmEmail(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label htmlFor="password">Password (at least 8 characters)</label>
|
<label htmlFor="password">Password (at least 8 characters)</label>
|
||||||
<input name="password" id="password"
|
<input name="password" id="password"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
type={(this.state.show_password)? 'text': 'password'}
|
type={(showPassword)? 'text': 'password'}
|
||||||
value={this.state.password} onChange={this.handleChange}
|
value={password} onChange={e => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
minLength={8}
|
minLength={8}
|
||||||
maxLength={128}
|
maxLength={128}
|
||||||
@ -122,8 +79,8 @@ class SignUp extends Component<SignUpProps, SignUpState> {
|
|||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
<input id="show_password" name="show_password"
|
<input id="show_password" name="show_password"
|
||||||
className="form-check-input" type="checkbox"
|
className="form-check-input" type="checkbox"
|
||||||
checked={this.state.show_password}
|
checked={showPassword}
|
||||||
onChange={this.handleChange}
|
onChange={e => setShowPassword(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<label className="form-check-label" htmlFor="show_password">
|
<label className="form-check-label" htmlFor="show_password">
|
||||||
Show password?
|
Show password?
|
||||||
@ -133,8 +90,8 @@ class SignUp extends Component<SignUpProps, SignUpState> {
|
|||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
<input id="confirm_conditions" name="confirm_conditions"
|
<input id="confirm_conditions" name="confirm_conditions"
|
||||||
className="form-check-input" type="checkbox"
|
className="form-check-input" type="checkbox"
|
||||||
checked={this.state.confirm_conditions}
|
checked={confirmConditions}
|
||||||
onChange={this.handleChange}
|
onChange={e => setConfirmConditions(e.target.checked)}
|
||||||
required />
|
required />
|
||||||
<label className="form-check-label" htmlFor="confirm_conditions">
|
<label className="form-check-label" htmlFor="confirm_conditions">
|
||||||
I confirm that I have read and agree to the <Link
|
I confirm that I have read and agree to the <Link
|
||||||
@ -145,7 +102,8 @@ class SignUp extends Component<SignUpProps, SignUpState> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="buttons-container with-space">
|
<div className="buttons-container with-space">
|
||||||
<input type="submit" value="Sign Up" className="btn btn-primary" />
|
<input type="submit" disabled={isLoading} value="Sign Up" className="btn btn-primary" />
|
||||||
|
{isLoading && <span><SpinnerIcon/>Sending sign up data...</span>}
|
||||||
</div>
|
</div>
|
||||||
<InfoBox msg="">
|
<InfoBox msg="">
|
||||||
Please also read our <a href="https://www.pages.colouring.london/data-ethics">data ethics policy</a> before using or sharing our data
|
Please also read our <a href="https://www.pages.colouring.london/data-ethics">data ethics policy</a> before using or sharing our data
|
||||||
@ -165,7 +123,4 @@ class SignUp extends Component<SignUpProps, SignUpState> {
|
|||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default SignUp;
|
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
getUserVerifiedAttributes
|
getUserVerifiedAttributes
|
||||||
} from './api/services/building/base';
|
} from './api/services/building/base';
|
||||||
import { getUserById } from './api/services/user';
|
import { getUserById } from './api/services/user';
|
||||||
import App from './frontend/app';
|
import { App } from './frontend/app';
|
||||||
import { parseBuildingURL } from './parse';
|
import { parseBuildingURL } from './parse';
|
||||||
import asyncController from './api/routes/asyncController';
|
import asyncController from './api/routes/asyncController';
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user