Dwayne Harris
5 years ago
81 changed files with 1306 additions and 1578 deletions
-
278package-lock.json
-
34package.json
-
48src/actions/apps.ts
-
14src/actions/groups.ts
-
106src/components/app.tsx
-
79src/components/app/app.tsx
-
28src/components/app/index.ts
-
23src/components/create-group-form.tsx
-
26src/components/create-group-form/index.ts
-
83src/components/create-group-step.tsx
-
55src/components/create-group-step/create-group-step.tsx
-
49src/components/create-group-step/index.ts
-
24src/components/create-user-form.tsx
-
26src/components/create-user-form/index.ts
-
101src/components/create-user-step.tsx
-
54src/components/create-user-step/create-user-step.tsx
-
71src/components/create-user-step/index.ts
-
0src/components/footer.tsx
-
25src/components/forms/checkbox-field.tsx
-
24src/components/forms/checkbox-field/checkbox-field.tsx
-
23src/components/forms/checkbox-field/index.ts
-
24src/components/forms/password-field.tsx
-
24src/components/forms/password-field/index.ts
-
22src/components/forms/select-field.tsx
-
24src/components/forms/select-field/index.ts
-
29src/components/forms/text-field.tsx
-
24src/components/forms/text-field/index.ts
-
25src/components/forms/textarea-field.tsx
-
24src/components/forms/textarea-field/index.ts
-
0src/components/group-info.tsx
-
55src/components/group-invitations.tsx
-
38src/components/group-invitations/index.ts
-
0src/components/group-list-item.tsx
-
0src/components/group-list.tsx
-
27src/components/group-logs.tsx
-
26src/components/group-logs/index.tsx
-
0src/components/member-list-item.tsx
-
36src/components/member-list.tsx
-
26src/components/member-list/index.ts
-
25src/components/member-list/member-list.tsx
-
0src/components/navigation-menu.tsx
-
26src/components/notification-container.tsx
-
26src/components/notification-container/index.ts
-
6src/components/notification-container/notification-container.scss
-
0src/components/notification.tsx
-
0src/components/page-header.tsx
-
0src/components/pages/about.tsx
-
12src/components/pages/create-app.tsx
-
43src/components/pages/developers.tsx
-
22src/components/pages/directory/index.ts
-
94src/components/pages/group-admin.tsx
-
44src/components/pages/group-admin/index.ts
-
36src/components/pages/group.tsx
-
26src/components/pages/group/index.ts
-
26src/components/pages/groups.tsx
-
0src/components/pages/home.tsx
-
6src/components/pages/loading.tsx
-
101src/components/pages/login.tsx
-
56src/components/pages/login/index.ts
-
76src/components/pages/login/login.tsx
-
101src/components/pages/register-group.tsx
-
63src/components/pages/register-group/index.ts
-
61src/components/pages/register-group/register-group.tsx
-
89src/components/pages/register.tsx
-
52src/components/pages/register/register.tsx
-
60src/components/pages/self.tsx
-
35src/components/pages/self/index.ts
-
2src/components/spinner.tsx
-
25src/components/user-apps.tsx
-
16src/components/user-info.tsx
-
15src/components/user-info/index.ts
-
3src/components/user-info/user-info.scss
-
14src/reducers/groups.ts
-
18src/selectors/apps.ts
-
2src/selectors/groups.ts
-
4src/store/index.ts
-
43src/styles/app.scss
-
0src/styles/spinner.scss
-
29src/types/entities.ts
-
6src/types/store.ts
-
46src/utils/normalization.ts
@ -0,0 +1,48 @@ |
|||
import { apiFetch } from 'src/api' |
|||
import { setEntities } from 'src/actions/entities' |
|||
import { startRequest, finishRequest } from 'src/actions/requests' |
|||
import { objectToQuerystring } from 'src/utils' |
|||
import { normalize } from 'src/utils/normalization' |
|||
|
|||
import { AppThunkAction, RequestKey, EntityType, App }from 'src/types' |
|||
|
|||
interface AppsResponse { |
|||
apps: App[] |
|||
continuation?: string |
|||
} |
|||
|
|||
export const fetchApps = (sort?: string, continuation?: string): AppThunkAction => async dispatch => { |
|||
dispatch(startRequest(RequestKey.FetchApps)) |
|||
|
|||
try { |
|||
const response = await apiFetch<AppsResponse>({ |
|||
path: `/api/apps?${objectToQuerystring({ sort, continuation })}`, |
|||
}) |
|||
|
|||
const apps = normalize(response.apps, EntityType.App) |
|||
|
|||
dispatch(setEntities(apps.entities)) |
|||
dispatch(finishRequest(RequestKey.FetchApps, true)) |
|||
} catch (err) { |
|||
dispatch(finishRequest(RequestKey.FetchApps, false)) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
export const fetchSelfApps = (sort?: string): AppThunkAction => async dispatch => { |
|||
dispatch(startRequest(RequestKey.FetchSelfApps)) |
|||
|
|||
try { |
|||
const response = await apiFetch<AppsResponse>({ |
|||
path: `/api/self/apps?${objectToQuerystring({ sort })}`, |
|||
}) |
|||
|
|||
const apps = normalize(response.apps, EntityType.App) |
|||
|
|||
dispatch(setEntities(apps.entities)) |
|||
dispatch(finishRequest(RequestKey.FetchSelfApps, true)) |
|||
} catch (err) { |
|||
dispatch(finishRequest(RequestKey.FetchSelfApps, false)) |
|||
throw err |
|||
} |
|||
} |
@ -0,0 +1,106 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom' |
|||
|
|||
import { fetchSelf, setChecked } from 'src/actions/authentication' |
|||
import { getFetching } from 'src/selectors' |
|||
import { getCollapsed } from 'src/selectors/menu' |
|||
|
|||
import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from 'src/constants' |
|||
import { AppState, AppThunkDispatch } from 'src/types' |
|||
|
|||
import Footer from './footer' |
|||
import NavigationMenu from './navigation-menu' |
|||
import NotificationContainer from './notification-container' |
|||
import Spinner from './spinner' |
|||
import UserInfo from './user-info' |
|||
|
|||
import About from './pages/about' |
|||
import Developers from './pages/developers' |
|||
import Group from './pages/group' |
|||
import GroupAdmin from './pages/group-admin' |
|||
import Groups from './pages/groups' |
|||
import Home from './pages/home' |
|||
import Login from './pages/login' |
|||
import Register from './pages/register' |
|||
import RegisterGroup from './pages/register-group' |
|||
import Self from './pages/self' |
|||
|
|||
import '../styles/app.scss' |
|||
import '../styles/spinner.scss' |
|||
|
|||
const App: FC = () => { |
|||
const collapsed = useSelector<AppState, boolean>(getCollapsed) |
|||
const fetching = useSelector<AppState, boolean>(getFetching) |
|||
|
|||
const dispatch = useDispatch<AppThunkDispatch>() |
|||
|
|||
const mainMenuWidth = 275 |
|||
const mainColumnMargin = collapsed ? 0 : mainMenuWidth |
|||
|
|||
useEffect(() => { |
|||
if (localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)) { |
|||
dispatch(fetchSelf()) |
|||
} else { |
|||
dispatch(setChecked()) |
|||
} |
|||
}, []) |
|||
|
|||
return ( |
|||
<Router> |
|||
<div> |
|||
<div id="main-menu" style={{ width: mainMenuWidth }}> |
|||
<div id="header"> |
|||
<Link className="has-text-white is-size-3" to="/">Flexor</Link> |
|||
<hr className="has-background-grey-lighter" /> |
|||
</div> |
|||
|
|||
<NavigationMenu /> |
|||
|
|||
{fetching && <Spinner />} |
|||
<UserInfo /> |
|||
<Footer /> |
|||
</div> |
|||
|
|||
<div id="main-column" style={{ marginRight: mainColumnMargin }}> |
|||
<Switch> |
|||
<Route path="/"> |
|||
<Home /> |
|||
</Route> |
|||
<Route path="/login"> |
|||
<Login /> |
|||
</Route> |
|||
<Route path="/register"> |
|||
<Register /> |
|||
</Route> |
|||
<Route path="/c/:id"> |
|||
<Group /> |
|||
</Route> |
|||
<Route path="/c/:id/admin/:tab?"> |
|||
<GroupAdmin /> |
|||
</Route> |
|||
<Route path="/c/:id/register"> |
|||
<RegisterGroup /> |
|||
</Route> |
|||
<Route path="/communities"> |
|||
<Groups /> |
|||
</Route> |
|||
<Route path="/self/:tab?"> |
|||
<Self /> |
|||
</Route> |
|||
<Route path="/developers"> |
|||
<Developers /> |
|||
</Route> |
|||
<Route path="/about"> |
|||
<About /> |
|||
</Route> |
|||
</Switch> |
|||
</div> |
|||
|
|||
<NotificationContainer /> |
|||
</div> |
|||
</Router> |
|||
) |
|||
} |
|||
|
|||
export default App |
@ -1,79 +0,0 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { BrowserRouter as Router, Route, Link } from 'react-router-dom' |
|||
|
|||
import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from 'src/constants' |
|||
|
|||
import Footer from '../footer' |
|||
import NavigationMenu from '../navigation-menu' |
|||
import NotificationContainer from '../notification-container' |
|||
import Spinner from '../spinner' |
|||
import UserInfo from '../user-info' |
|||
|
|||
import About from '../pages/about' |
|||
import Developers from '../pages/developers' |
|||
import Directory from '../pages/directory' |
|||
import Group from '../pages/group' |
|||
import GroupAdmin from '../pages/group-admin' |
|||
import Home from '../pages/home' |
|||
import Login from '../pages/login' |
|||
import Register from '../pages/register' |
|||
import RegisterGroup from '../pages/register-group' |
|||
import Self from '../pages/self' |
|||
|
|||
import './app.scss' |
|||
|
|||
interface Props { |
|||
collapsed: boolean |
|||
fetching: boolean |
|||
fetchSelf: () => void |
|||
setChecked: () => void |
|||
} |
|||
|
|||
const App: FC<Props> = ({ collapsed, fetching, fetchSelf, setChecked }) => { |
|||
const mainMenuWidth = 275 |
|||
const mainColumnMargin = collapsed ? 0 : mainMenuWidth |
|||
|
|||
useEffect(() => { |
|||
if (localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)) { |
|||
fetchSelf() |
|||
} else { |
|||
setChecked() |
|||
} |
|||
}, []) |
|||
|
|||
return ( |
|||
<Router> |
|||
<div> |
|||
<div id="main-menu" style={{ width: mainMenuWidth }}> |
|||
<div id="header"> |
|||
<Link className="has-text-white is-size-3" to="/">Flexor</Link> |
|||
<hr className="has-background-grey-lighter" /> |
|||
</div> |
|||
|
|||
<NavigationMenu /> |
|||
|
|||
{fetching && <Spinner />} |
|||
<UserInfo /> |
|||
<Footer /> |
|||
</div> |
|||
|
|||
<div id="main-column" style={{ marginRight: mainColumnMargin }}> |
|||
<Route exact path="/" component={Home} /> |
|||
<Route path="/login" component={Login} /> |
|||
<Route path="/register" component={Register} /> |
|||
<Route path="/c/:id" component={Group} exact /> |
|||
<Route path="/c/:id/admin/:tab?" component={GroupAdmin} /> |
|||
<Route path="/c/:id/register" component={RegisterGroup} /> |
|||
<Route path="/communities" component={Directory} /> |
|||
<Route path="/self/:tab?" component={Self} /> |
|||
<Route path="/developers" component={Developers} /> |
|||
<Route path="/about" component={About} /> |
|||
</div> |
|||
|
|||
<NotificationContainer /> |
|||
</div> |
|||
</Router> |
|||
) |
|||
} |
|||
|
|||
export default App |
@ -1,28 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { fetchSelf, setChecked } from 'src/actions/authentication' |
|||
import { getFetching } from 'src/selectors' |
|||
import { getCollapsed } from 'src/selectors/menu' |
|||
|
|||
import { AppState, AppThunkDispatch } from 'src/types' |
|||
|
|||
import App from './app' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
collapsed: getCollapsed(state), |
|||
fetching: getFetching(state), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({ |
|||
fetchSelf: () => { |
|||
dispatch(fetchSelf()) |
|||
}, |
|||
setChecked: () => { |
|||
dispatch(setChecked()) |
|||
} |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(App) |
@ -1,26 +0,0 @@ |
|||
import { FocusEventHandler } from 'react' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { checkGroupAvailability } from 'src/actions/registration' |
|||
import { AppThunkDispatch } from 'src/types' |
|||
|
|||
import CreateGroupForm from './create-group-form' |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch) => { |
|||
const checkAvailability: FocusEventHandler<HTMLInputElement> = event => { |
|||
const value = event.target.value |
|||
|
|||
if (value.length > 3) { |
|||
dispatch(checkGroupAvailability(event.target.value)) |
|||
} |
|||
} |
|||
|
|||
return { |
|||
checkAvailability, |
|||
} |
|||
} |
|||
|
|||
export default connect( |
|||
null, |
|||
mapDispatchToProps |
|||
)(CreateGroupForm) |
@ -0,0 +1,83 @@ |
|||
import React, { FC } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faBuilding, faArrowLeft } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import { setFieldNotification } from 'src/actions/forms' |
|||
import { showNotification } from 'src/actions/notifications' |
|||
import { setStep } from 'src/actions/registration' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
|
|||
import { MAX_ID_LENGTH } from 'src/constants' |
|||
import { AppState, AppThunkDispatch, NotificationType } from 'src/types' |
|||
|
|||
import CreateGroupForm from './create-group-form' |
|||
|
|||
interface Props { |
|||
register: () => void |
|||
} |
|||
|
|||
const CreateGroupStep: FC<Props> = ({ register }) => { |
|||
const name = useSelector<AppState, string>(state => getFieldValue<string>(state, 'group-name', '')) |
|||
const registration = useSelector<AppState, string>(state => getFieldValue<string>(state, 'group-registration', '')) |
|||
const agree = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, 'group-agree', false)) |
|||
|
|||
const dispatch = useDispatch<AppThunkDispatch>() |
|||
|
|||
const next = (name: string, registration: string, agree: boolean) => { |
|||
let invalid = false |
|||
|
|||
if (!name) { |
|||
dispatch(setFieldNotification('group-name', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (name.length > MAX_ID_LENGTH) { |
|||
dispatch(setFieldNotification('group-name', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`)) |
|||
invalid = true |
|||
} |
|||
|
|||
if (!agree) { |
|||
dispatch(setFieldNotification('group-agree', NotificationType.Error, 'You must agree to the terms and conditions to continue')) |
|||
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions to continue.')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (invalid) return |
|||
register() |
|||
} |
|||
|
|||
return ( |
|||
<div className="centered-content"> |
|||
<div className="centered-content-icon has-background-primary"> |
|||
<span className="icon is-large has-text-white"> |
|||
<FontAwesomeIcon icon={faBuilding} size="2x" /> |
|||
</span> |
|||
</div> |
|||
|
|||
<CreateGroupForm /> |
|||
<hr /> |
|||
|
|||
<nav className="level"> |
|||
<div className="level-left"> |
|||
<p className="level-item"> |
|||
<button className="button" onClick={() => dispatch(setStep(0))}> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faArrowLeft} /> |
|||
</span> |
|||
<span>Your Account</span> |
|||
</button> |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="level-right"> |
|||
<p className="level-item"> |
|||
<button className="button is-success" onClick={() => next(name, registration, agree)}>Finish</button> |
|||
</p> |
|||
</div> |
|||
</nav> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default CreateGroupStep |
@ -1,55 +0,0 @@ |
|||
import React, { FC } from 'react' |
|||
import noop from 'lodash/noop' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faBuilding, faArrowLeft } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import CreateGroupForm from '../create-group-form' |
|||
|
|||
export interface Props { |
|||
name?: string |
|||
registration?: string |
|||
agree?: boolean |
|||
previous?: () => void |
|||
next?: (name: string, registration: string, agree: boolean) => void |
|||
register: () => void |
|||
} |
|||
|
|||
const CreateGroupStep: FC<Props> = ({ |
|||
name = '', |
|||
registration = '', |
|||
agree = false, |
|||
previous = noop, |
|||
next = noop, |
|||
}) => ( |
|||
<div className="centered-content"> |
|||
<div className="centered-content-icon has-background-primary"> |
|||
<span className="icon is-large has-text-white"> |
|||
<FontAwesomeIcon icon={faBuilding} size="2x" /> |
|||
</span> |
|||
</div> |
|||
|
|||
<CreateGroupForm /> |
|||
<hr /> |
|||
|
|||
<nav className="level"> |
|||
<div className="level-left"> |
|||
<p className="level-item"> |
|||
<button className="button" onClick={() => previous()}> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faArrowLeft} /> |
|||
</span> |
|||
<span>Your Account</span> |
|||
</button> |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="level-right"> |
|||
<p className="level-item"> |
|||
<button className="button is-success" onClick={() => next(name, registration, agree)}>Finish</button> |
|||
</p> |
|||
</div> |
|||
</nav> |
|||
</div> |
|||
) |
|||
|
|||
export default CreateGroupStep |
@ -1,49 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { setFieldNotification } from 'src/actions/forms' |
|||
import { showNotification } from 'src/actions/notifications' |
|||
import { setStep } from 'src/actions/registration' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
import { MAX_ID_LENGTH } from 'src/constants' |
|||
import { AppState, AppThunkDispatch, NotificationType } from 'src/types' |
|||
|
|||
import CreateGroupStep, { Props } from './create-group-step' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
name: getFieldValue<string>(state, 'group-name', ''), |
|||
registration: getFieldValue<string>(state, 'group-registration', ''), |
|||
agree: getFieldValue<boolean>(state, 'group-agree', false), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ |
|||
previous: () => { |
|||
dispatch(setStep(0)) |
|||
}, |
|||
next: (name: string, registration: string, agree: boolean) => { |
|||
let invalid = false |
|||
|
|||
if (!name) { |
|||
dispatch(setFieldNotification('group-name', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (name.length > MAX_ID_LENGTH) { |
|||
dispatch(setFieldNotification('group-name', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`)) |
|||
invalid = true |
|||
} |
|||
|
|||
if (!agree) { |
|||
dispatch(setFieldNotification('group-agree', NotificationType.Error, 'You must agree to the terms and conditions to continue')) |
|||
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions to continue.')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (invalid) return |
|||
if (ownProps.register) ownProps.register() |
|||
}, |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(CreateGroupStep) |
@ -1,19 +1,25 @@ |
|||
import React, { FC, FocusEventHandler } from 'react' |
|||
import React, { FC } from 'react' |
|||
import { useDispatch } from 'react-redux' |
|||
import { Link } from 'react-router-dom' |
|||
import { faEnvelope, faIdCard } from '@fortawesome/free-solid-svg-icons' |
|||
import { checkUserAvailability } from 'src/actions/registration' |
|||
|
|||
import CheckboxField from '../forms/checkbox-field' |
|||
import TextField from '../forms/text-field' |
|||
import PasswordField from '../forms/password-field' |
|||
import CheckboxField from './forms/checkbox-field' |
|||
import TextField from './forms/text-field' |
|||
import PasswordField from './forms/password-field' |
|||
|
|||
interface Props { |
|||
checkAvailability: FocusEventHandler<HTMLInputElement> |
|||
} |
|||
const CreateUserForm: FC = () => { |
|||
const dispatch = useDispatch() |
|||
|
|||
const checkAvailability = (value: string) => { |
|||
if (value.length > 3) { |
|||
dispatch(checkUserAvailability(value)) |
|||
} |
|||
} |
|||
|
|||
const CreateUserForm: FC<Props> = ({ checkAvailability }) => { |
|||
return ( |
|||
<div className="container"> |
|||
<TextField icon={faIdCard} name="user-id" label="Username" placeholder="Your Username/ID" onBlur={checkAvailability} /> |
|||
<TextField icon={faIdCard} name="user-id" label="Username" placeholder="Your Username/ID" onBlur={e => checkAvailability(e.target.value)} /> |
|||
<br /> |
|||
<TextField name="user-name" label="Display Name" placeholder="Whatever you want to go by" /> |
|||
<br /> |
@ -1,26 +0,0 @@ |
|||
import { FocusEventHandler } from 'react' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { checkUserAvailability } from 'src/actions/registration' |
|||
import { AppThunkDispatch } from 'src/types' |
|||
|
|||
import CreateUserForm from './create-user-form' |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch) => { |
|||
const checkAvailability: FocusEventHandler<HTMLInputElement> = event => { |
|||
const value = event.target.value |
|||
|
|||
if (value.length > 3) { |
|||
dispatch(checkUserAvailability(event.target.value)) |
|||
} |
|||
} |
|||
|
|||
return { |
|||
checkAvailability, |
|||
} |
|||
} |
|||
|
|||
export default connect( |
|||
null, |
|||
mapDispatchToProps |
|||
)(CreateUserForm) |
@ -0,0 +1,101 @@ |
|||
import React, { FC } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import zxcvbn from 'zxcvbn' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faUser, faArrowRight } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import { setFieldNotification } from 'src/actions/forms' |
|||
import { showNotification } from 'src/actions/notifications' |
|||
import { setStep } from 'src/actions/registration' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
import { MAX_ID_LENGTH, MAX_NAME_LENGTH } from 'src/constants' |
|||
import { AppState, AppThunkDispatch, NotificationType } from 'src/types' |
|||
|
|||
import CreateUserForm from './create-user-form' |
|||
|
|||
const CreateUserStep: FC = () => { |
|||
const userId = useSelector<AppState, string>(state => getFieldValue<string>(state, 'user-id', '')) |
|||
const name = useSelector<AppState, string>(state => getFieldValue<string>(state, 'user-name', '')) |
|||
const email = useSelector<AppState, string>(state => getFieldValue<string>(state, 'user-email', '')) |
|||
const password = useSelector<AppState, string>(state => getFieldValue<string>(state, 'password', '')) |
|||
const agree = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, 'user-agree', false)) |
|||
|
|||
const dispatch = useDispatch<AppThunkDispatch>() |
|||
|
|||
const next = (userId: string, name: string, email: string, password: string, agree: boolean) => { |
|||
let invalid = false |
|||
|
|||
if (!userId) { |
|||
dispatch(setFieldNotification('user-id', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (userId.length > MAX_ID_LENGTH) { |
|||
dispatch(setFieldNotification('user-id', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`)) |
|||
invalid = true |
|||
} |
|||
|
|||
if (name.length > MAX_NAME_LENGTH) { |
|||
dispatch(setFieldNotification('user-name', NotificationType.Error, `This must be less than ${MAX_NAME_LENGTH} characters`)) |
|||
invalid = true |
|||
} |
|||
|
|||
if (email === '') { |
|||
dispatch(setFieldNotification('user-email', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (!agree) { |
|||
dispatch(setFieldNotification('user-agree', NotificationType.Error, 'You must agree to the terms and conditions to continue')) |
|||
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions to continue.')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (password === '') { |
|||
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} else { |
|||
const { score } = zxcvbn(password) |
|||
|
|||
if (score === 0) { |
|||
dispatch(setFieldNotification('password', NotificationType.Error, 'Try another password')) |
|||
invalid = true |
|||
} |
|||
} |
|||
|
|||
if (invalid) return |
|||
dispatch(setStep(1)) |
|||
} |
|||
|
|||
return ( |
|||
<div className="centered-content"> |
|||
<div className="centered-content-icon has-background-primary"> |
|||
<span className="icon is-large has-text-white"> |
|||
<FontAwesomeIcon icon={faUser} size="2x" /> |
|||
</span> |
|||
</div> |
|||
|
|||
<CreateUserForm /> |
|||
<hr /> |
|||
|
|||
<nav className="level"> |
|||
<div className="level-left"> |
|||
<p className="level-item"></p> |
|||
</div> |
|||
|
|||
<div className="level-right"> |
|||
<p className="level-item"> |
|||
<button className="button is-success" onClick={() => next(userId, name, email, password, agree)}> |
|||
<span>Community</span> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faArrowRight} /> |
|||
</span> |
|||
</button> |
|||
</p> |
|||
</div> |
|||
</nav> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default CreateUserStep |
@ -1,54 +0,0 @@ |
|||
import React, { FC } from 'react' |
|||
import noop from 'lodash/noop' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faUser, faArrowRight } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import CreateUserForm from '../create-user-form' |
|||
|
|||
export interface Props { |
|||
userId?: string |
|||
name?: string |
|||
email?: string |
|||
password?: string |
|||
agree?: boolean |
|||
next?: (userId: string, name: string, email: string, password: string, agree: boolean) => void |
|||
} |
|||
|
|||
const CreateUserStep: FC<Props> = ({ |
|||
userId = '', |
|||
name = '', |
|||
email = '', |
|||
password = '', |
|||
agree = false, |
|||
next = noop, |
|||
}) => ( |
|||
<div className="centered-content"> |
|||
<div className="centered-content-icon has-background-primary"> |
|||
<span className="icon is-large has-text-white"> |
|||
<FontAwesomeIcon icon={faUser} size="2x" /> |
|||
</span> |
|||
</div> |
|||
|
|||
<CreateUserForm /> |
|||
<hr /> |
|||
|
|||
<nav className="level"> |
|||
<div className="level-left"> |
|||
<p className="level-item"></p> |
|||
</div> |
|||
|
|||
<div className="level-right"> |
|||
<p className="level-item"> |
|||
<button className="button is-success" onClick={() => next(userId, name, email, password, agree)}> |
|||
<span>Community</span> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faArrowRight} /> |
|||
</span> |
|||
</button> |
|||
</p> |
|||
</div> |
|||
</nav> |
|||
</div> |
|||
) |
|||
|
|||
export default CreateUserStep |
@ -1,71 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
import zxcvbn from 'zxcvbn' |
|||
|
|||
import { setFieldNotification } from 'src/actions/forms' |
|||
import { showNotification } from 'src/actions/notifications' |
|||
import { setStep } from 'src/actions/registration' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
import { MAX_ID_LENGTH, MAX_NAME_LENGTH } from 'src/constants' |
|||
import { AppState, AppThunkDispatch, NotificationType } from 'src/types' |
|||
|
|||
import CreateUserStep from './create-user-step' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
userId: getFieldValue<string>(state, 'user-id', ''), |
|||
name: getFieldValue<string>(state, 'user-name', ''), |
|||
email: getFieldValue<string>(state, 'user-email', ''), |
|||
password: getFieldValue<string>(state, 'password', ''), |
|||
agree: getFieldValue<boolean>(state, 'user-agree', false), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({ |
|||
next: (userId: string, name: string, email: string, password: string, agree: boolean) => { |
|||
let invalid = false |
|||
|
|||
if (!userId) { |
|||
dispatch(setFieldNotification('user-id', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (userId.length > MAX_ID_LENGTH) { |
|||
dispatch(setFieldNotification('user-id', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`)) |
|||
invalid = true |
|||
} |
|||
|
|||
if (name.length > MAX_NAME_LENGTH) { |
|||
dispatch(setFieldNotification('user-name', NotificationType.Error, `This must be less than ${MAX_NAME_LENGTH} characters`)) |
|||
invalid = true |
|||
} |
|||
|
|||
if (email === '') { |
|||
dispatch(setFieldNotification('user-email', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (!agree) { |
|||
dispatch(setFieldNotification('user-agree', NotificationType.Error, 'You must agree to the terms and conditions to continue')) |
|||
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions to continue.')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (password === '') { |
|||
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} else { |
|||
const { score } = zxcvbn(password) |
|||
|
|||
if (score === 0) { |
|||
dispatch(setFieldNotification('password', NotificationType.Error, 'Try another password')) |
|||
invalid = true |
|||
} |
|||
} |
|||
|
|||
if (invalid) return |
|||
dispatch(setStep(1)) |
|||
}, |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(CreateUserStep) |
@ -0,0 +1,25 @@ |
|||
import React, { FC } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
|
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
import { AppState } from 'src/types' |
|||
|
|||
interface Props { |
|||
name: string |
|||
} |
|||
|
|||
const CheckboxField: FC<Props> = ({ name, children }) => { |
|||
const value = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, name, false)) |
|||
const dispatch = useDispatch() |
|||
|
|||
return ( |
|||
<label className="checkbox"> |
|||
<input type="checkbox" checked={value} onChange={e => dispatch(setFieldValue(name, e.target.checked))} /> |
|||
|
|||
{children} |
|||
</label> |
|||
) |
|||
} |
|||
|
|||
export default CheckboxField |
@ -1,24 +0,0 @@ |
|||
import React, { FC } from 'react' |
|||
import noop from 'lodash/noop' |
|||
|
|||
export interface Props { |
|||
name: string |
|||
value?: boolean |
|||
setValue?: (value: boolean) => void |
|||
} |
|||
|
|||
const PasswordField: FC<Props> = ({ |
|||
value = false, |
|||
setValue = noop, |
|||
children, |
|||
}) => { |
|||
return ( |
|||
<label className="checkbox"> |
|||
<input type="checkbox" checked={value} onChange={(e) => setValue(e.target.checked)} /> |
|||
|
|||
{children} |
|||
</label> |
|||
) |
|||
} |
|||
|
|||
export default PasswordField |
@ -1,23 +0,0 @@ |
|||
import { Dispatch } from 'redux' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
import { AppState } from 'src/types' |
|||
|
|||
import CheckboxField, { Props } from './checkbox-field' |
|||
|
|||
const mapStateToProps = (state: AppState, ownProps: Props) => ({ |
|||
value: getFieldValue<boolean>(state, ownProps.name, false), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({ |
|||
setValue: (value: boolean) => { |
|||
dispatch(setFieldValue(ownProps.name, value)) |
|||
} |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(CheckboxField) |
@ -1,24 +0,0 @@ |
|||
import { Dispatch } from 'redux' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue, getFieldNotification } from 'src/selectors/forms' |
|||
import { AppState } from 'src/types' |
|||
|
|||
import PasswordField from './password-field' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
value: getFieldValue<string>(state, 'password', ''), |
|||
notification: getFieldNotification(state, 'password'), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: Dispatch) => ({ |
|||
setValue: (value: string) => { |
|||
dispatch(setFieldValue('password', value)) |
|||
} |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(PasswordField) |
@ -1,24 +0,0 @@ |
|||
import { Dispatch } from 'redux' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue, getFieldNotification } from 'src/selectors/forms' |
|||
import { AppState } from 'src/types' |
|||
|
|||
import SelectField, { Props } from './select-field' |
|||
|
|||
const mapStateToProps = (state: AppState, ownProps: Props) => ({ |
|||
value: getFieldValue<string>(state, ownProps.name, ''), |
|||
notification: getFieldNotification(state, ownProps.name), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({ |
|||
setValue: (value: string) => { |
|||
dispatch(setFieldValue(ownProps.name, value)) |
|||
} |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(SelectField) |
@ -1,24 +0,0 @@ |
|||
import { Dispatch } from 'redux' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue, getFieldNotification } from 'src/selectors/forms' |
|||
import { AppState } from 'src/types' |
|||
|
|||
import TextField, { Props } from './text-field' |
|||
|
|||
const mapStateToProps = (state: AppState, ownProps: Props) => ({ |
|||
value: getFieldValue<string>(state, ownProps.name, ''), |
|||
notification: getFieldNotification(state, ownProps.name), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({ |
|||
setValue: (value: string) => { |
|||
dispatch(setFieldValue(ownProps.name, value)) |
|||
} |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(TextField) |
@ -1,24 +0,0 @@ |
|||
import { Dispatch } from 'redux' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue, getFieldNotification } from 'src/selectors/forms' |
|||
import { AppState } from 'src/types' |
|||
|
|||
import TextareaField, { Props } from './textarea-field' |
|||
|
|||
const mapStateToProps = (state: AppState, ownProps: Props) => ({ |
|||
value: getFieldValue<string>(state, ownProps.name, ''), |
|||
notification: getFieldNotification(state, ownProps.name), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => ({ |
|||
setValue: (value: string) => { |
|||
dispatch(setFieldValue(ownProps.name, value)) |
|||
} |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(TextareaField) |
@ -1,38 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
import moment from 'moment' |
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchInvitations, createInvitation } from 'src/actions/directory' |
|||
import { getInvitations } from 'src/selectors/directory' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
import { AppState, AppThunkDispatch } from 'src/types' |
|||
|
|||
import GroupInvitations, { Props } from './group-invitations' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
invitations: getInvitations(state), |
|||
expiration: getFieldValue<string>(state, 'expiration', '0'), |
|||
limit: getFieldValue<string>(state, 'limit', '0'), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ |
|||
fetchInvitations: () => { |
|||
try { |
|||
dispatch(fetchInvitations(ownProps.group)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch) |
|||
} |
|||
}, |
|||
createInvitation: async (expiration: string, limit: string) => { |
|||
try { |
|||
await dispatch(createInvitation(ownProps.group, moment().add(expiration, 'day').valueOf(), parseInt(limit, 10))) |
|||
await dispatch(fetchInvitations(ownProps.group)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch) |
|||
} |
|||
}, |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(GroupInvitations) |
@ -1,19 +1,30 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { Link } from 'react-router-dom' |
|||
import noop from 'lodash/noop' |
|||
import moment from 'moment' |
|||
import { GroupLog } from 'src/types' |
|||
|
|||
export interface Props { |
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchLogs } from 'src/actions/groups' |
|||
import { getLogs } from 'src/selectors/groups' |
|||
import { AppState, GroupLog } from 'src/types' |
|||
|
|||
interface Props { |
|||
group: string |
|||
logs?: GroupLog[] |
|||
fetchLogs?: () => void |
|||
} |
|||
|
|||
const MemberList: FC<Props> = ({ group, logs = [], fetchLogs = noop }) => { |
|||
const MemberList: FC<Props> = ({ group }) => { |
|||
const logs = useSelector<AppState, GroupLog[]>(getLogs) |
|||
const dispatch = useDispatch() |
|||
|
|||
useEffect(() => { |
|||
if (logs.length === 0) fetchLogs() |
|||
}, [group, fetchLogs]) |
|||
if (logs.length === 0) { |
|||
try { |
|||
dispatch(fetchLogs(group)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch) |
|||
} |
|||
} |
|||
}, [group]) |
|||
|
|||
return ( |
|||
<table className="table is-bordered is-striped is-narrow is-hoverable is-fullwidth"> |
@ -1,26 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchLogs } from 'src/actions/directory' |
|||
import { getLogs } from 'src/selectors/directory' |
|||
import { AppState, AppThunkDispatch } from 'src/types' |
|||
|
|||
import GroupLogs, { Props } from './group-logs' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
logs: getLogs(state), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ |
|||
fetchLogs: () => { |
|||
try { |
|||
dispatch(fetchLogs(ownProps.group)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch) |
|||
} |
|||
}, |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(GroupLogs) |
@ -0,0 +1,36 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { useHistory } from 'react-router-dom' |
|||
|
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchGroupMembers } from 'src/actions/groups' |
|||
import { getGroupMembers } from 'src/selectors/groups' |
|||
import { AppState, User, AppThunkDispatch } from 'src/types' |
|||
|
|||
import MemberListItem from './member-list-item' |
|||
|
|||
export interface Props { |
|||
group: string |
|||
} |
|||
|
|||
const MemberList: FC<Props> = ({ group }) => { |
|||
const members = useSelector<AppState, User[]>(state => getGroupMembers(state, group)) |
|||
const dispatch = useDispatch<AppThunkDispatch>() |
|||
const history = useHistory() |
|||
|
|||
useEffect(() => { |
|||
try { |
|||
dispatch(fetchGroupMembers(group)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, history) |
|||
} |
|||
}, [group]) |
|||
|
|||
return ( |
|||
<div className="is-flex"> |
|||
{members.map(member => <MemberListItem key={member.id} member={member} />)} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default MemberList |
@ -1,26 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchGroupMembers } from 'src/actions/directory' |
|||
import { getGroupMembers } from 'src/selectors/directory' |
|||
import { AppState, AppThunkDispatch } from 'src/types' |
|||
|
|||
import MemberList, { Props } from './member-list' |
|||
|
|||
const mapStateToProps = (state: AppState, ownProps: Props) => ({ |
|||
members: getGroupMembers(state, ownProps.group), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ |
|||
fetchGroupMembers: () => { |
|||
try { |
|||
dispatch(fetchGroupMembers(ownProps.group)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch) |
|||
} |
|||
}, |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(MemberList) |
@ -1,25 +0,0 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import noop from 'lodash/noop' |
|||
import { User } from 'src/types' |
|||
|
|||
import MemberListItem from './member-list-item' |
|||
|
|||
export interface Props { |
|||
group: string |
|||
members?: User[] |
|||
fetchGroupMembers?: () => void |
|||
} |
|||
|
|||
const MemberList: FC<Props> = ({ group, members = [], fetchGroupMembers = noop }) => { |
|||
useEffect(() => { |
|||
fetchGroupMembers() |
|||
}, [group, fetchGroupMembers]) |
|||
|
|||
return ( |
|||
<div className="is-flex"> |
|||
{members.map(member => <MemberListItem key={member.id} member={member} />)} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default MemberList |
@ -1,18 +1,26 @@ |
|||
import React, { FC } from 'react' |
|||
import { Notification as INotification } from 'src/types' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faDoorOpen } from '@fortawesome/free-solid-svg-icons' |
|||
import Notification from '../notification' |
|||
|
|||
import './notification-container.scss' |
|||
import { setNotificationAuto, removeNotification } from 'src/actions/notifications' |
|||
import { getNotifications } from 'src/selectors' |
|||
import { AppState, Notification as INotification } from 'src/types' |
|||
|
|||
interface Props { |
|||
notifications: INotification[] |
|||
setAuto: (id: string) => void |
|||
dismiss: (id: string) => void |
|||
} |
|||
import Notification from './notification' |
|||
|
|||
const NotificationContainer: FC = () => { |
|||
const notifications = useSelector<AppState, INotification[]>(getNotifications) |
|||
const dispatch = useDispatch() |
|||
|
|||
const setAuto = (id: string) => { |
|||
dispatch(setNotificationAuto(id)) |
|||
} |
|||
|
|||
const dismiss = (id: string) => { |
|||
dispatch(removeNotification(id)) |
|||
} |
|||
|
|||
const NotificationContainer: FC<Props> = ({ notifications, setAuto, dismiss }) => { |
|||
return ( |
|||
<div id="notification-container"> |
|||
{notifications.map(notification => { |
@ -1,26 +0,0 @@ |
|||
import { Dispatch } from 'redux' |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { setNotificationAuto, removeNotification } from 'src/actions/notifications' |
|||
import { getNotifications } from 'src/selectors' |
|||
import { AppState } from 'src/types' |
|||
|
|||
import NotificationContainer from './notification-container' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
notifications: getNotifications(state), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: Dispatch) => ({ |
|||
setAuto: (id: string) => { |
|||
dispatch(setNotificationAuto(id)) |
|||
}, |
|||
dismiss: (id: string) => { |
|||
dispatch(removeNotification(id)) |
|||
} |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(NotificationContainer) |
@ -1,6 +0,0 @@ |
|||
div#notification-container { |
|||
bottom: 10px; |
|||
position: absolute; |
|||
left: 10px; |
|||
width: 40%; |
|||
} |
@ -0,0 +1,43 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { Link } from 'react-router-dom' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import { fetchSelfApps } from 'src/actions/apps' |
|||
import { getSelfApps } from 'src/selectors/apps' |
|||
import { setTitle } from 'src/utils' |
|||
import { AppState, App, AppThunkDispatch } from 'src/types' |
|||
|
|||
import PageHeader from 'src/components/page-header' |
|||
|
|||
const Developers: FC = () => { |
|||
const apps = useSelector<AppState, App[]>(getSelfApps) |
|||
const dispatch = useDispatch<AppThunkDispatch>() |
|||
|
|||
useEffect(() => { |
|||
setTitle('Developers') |
|||
dispatch(fetchSelfApps()) |
|||
}, []) |
|||
|
|||
return ( |
|||
<div> |
|||
<PageHeader title="Developers" /> |
|||
|
|||
<div className="main-content"> |
|||
<div className="centered-content"> |
|||
<p>This is where you manage apps you create.</p> |
|||
|
|||
<Link className="button is-primary" to="/apps/new"> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faPlusCircle} /> |
|||
</span> |
|||
<span>Create a new App</span> |
|||
</Link> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default Developers |
@ -1,22 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { fetchGroups } from 'src/actions/directory' |
|||
import { getGroups } from 'src/selectors/directory' |
|||
import { AppState, AppThunkDispatch } from 'src/types' |
|||
|
|||
import Directory from './directory' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
groups: getGroups(state), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch) => ({ |
|||
fetchGroups: () => { |
|||
dispatch(fetchGroups()) |
|||
}, |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(Directory) |
@ -1,44 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchGroup, updateGroup } from 'src/actions/directory' |
|||
import { getEntity } from 'src/selectors/entities' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
import { AppState, EntityType, Group, AppThunkDispatch } from 'src/types' |
|||
|
|||
import GroupAdmin, { Props } from './group-admin' |
|||
import { initForm, initField, setFieldValue } from 'src/actions/forms' |
|||
|
|||
const mapStateToProps = (state: AppState, ownProps: Props) => ({ |
|||
group: getEntity<Group>(state, EntityType.Group, ownProps.match.params.id), |
|||
about: getFieldValue<string>(state, 'about'), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ |
|||
fetchGroup: () => { |
|||
try { |
|||
dispatch(fetchGroup(ownProps.match.params.id)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, ownProps.history) |
|||
} |
|||
}, |
|||
initForm: (group: Group) => { |
|||
dispatch(initForm()) |
|||
dispatch(initField('about')) |
|||
dispatch(initField('expiration')) |
|||
dispatch(initField('limit')) |
|||
dispatch(setFieldValue('about', group.about as string)) |
|||
}, |
|||
updateGroup: (about: string) => { |
|||
try { |
|||
dispatch(updateGroup(ownProps.match.params.id, { about })) |
|||
dispatch(fetchGroup(ownProps.match.params.id)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, ownProps.history) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(GroupAdmin) |
@ -1,41 +1,43 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { Link, RouteComponentProps } from 'react-router-dom' |
|||
|
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { Link, useParams, useHistory } from 'react-router-dom' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faEdit, faUserCheck } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchGroup } from 'src/actions/groups' |
|||
import { getEntity } from 'src/selectors/entities' |
|||
|
|||
import { setTitle } from 'src/utils' |
|||
import { Group, GroupMembershipType } from 'src/types' |
|||
import { AppState, EntityType, Group, GroupMembershipType, AppThunkDispatch } from 'src/types' |
|||
|
|||
import PageHeader from 'src/components/page-header' |
|||
import GroupInfo from 'src/components/group-info' |
|||
import Loading from 'src/components/pages/loading' |
|||
|
|||
interface Params { |
|||
id: string |
|||
} |
|||
|
|||
export interface Props extends RouteComponentProps<Params> { |
|||
group?: Group |
|||
fetchGroup: () => void |
|||
} |
|||
const GroupPage: FC = () => { |
|||
const { id } = useParams<Params>() |
|||
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id)) |
|||
const dispatch = useDispatch<AppThunkDispatch>() |
|||
const history = useHistory() |
|||
|
|||
const GroupPage: FC<Props> = ({ group, fetchGroup }) => { |
|||
useEffect(() => { |
|||
fetchGroup() |
|||
try { |
|||
dispatch(fetchGroup(id)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, history) |
|||
} |
|||
}, []) |
|||
|
|||
useEffect(() => { |
|||
if (group) setTitle(group.name) |
|||
}, [group]) |
|||
|
|||
if (!group) { |
|||
return ( |
|||
<div> |
|||
<PageHeader title="Group" /> |
|||
<div className="main-content"></div> |
|||
</div> |
|||
) |
|||
} |
|||
if (!group) return <Loading /> |
|||
|
|||
const isAdmin = group.membership === GroupMembershipType.Admin |
|||
|
@ -1,26 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchGroup } from 'src/actions/directory' |
|||
import { getEntity } from 'src/selectors/entities' |
|||
import { AppState, EntityType, Group, AppThunkDispatch } from 'src/types' |
|||
|
|||
import GroupPage, { Props } from './group' |
|||
|
|||
const mapStateToProps = (state: AppState, ownProps: Props) => ({ |
|||
group: getEntity<Group>(state, EntityType.Group, ownProps.match.params.id), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ |
|||
fetchGroup: () => { |
|||
try { |
|||
dispatch(fetchGroup(ownProps.match.params.id)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, ownProps.history) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(GroupPage) |
@ -1,10 +1,14 @@ |
|||
import React, { FC } from 'react' |
|||
|
|||
import PageHeader from 'src/components/page-header' |
|||
import Spinner from 'src/components/spinner' |
|||
|
|||
const Loading: FC = () => ( |
|||
<div> |
|||
<PageHeader title="Loading..." /> |
|||
<div className="main-content"></div> |
|||
<div className="main-content"> |
|||
<Spinner /> |
|||
</div> |
|||
</div> |
|||
) |
|||
|
@ -0,0 +1,101 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { useHistory } from 'react-router' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faIdCard, faKey } from '@fortawesome/free-solid-svg-icons' |
|||
import classNames from 'classnames' |
|||
|
|||
import { handleApiError } from 'src/api/errors' |
|||
import { authenticate } from 'src/actions/authentication' |
|||
import { showNotification } from 'src/actions/notifications' |
|||
import { getChecked, getAuthenticated } from 'src/selectors/authentication' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
import { getIsFetching } from 'src/selectors/requests' |
|||
import { initForm, initField, setFieldNotification } from 'src/actions/forms' |
|||
|
|||
import PageHeader from 'src/components/page-header' |
|||
import TextField from 'src/components/forms/text-field' |
|||
import PasswordField from 'src/components/forms/password-field' |
|||
|
|||
import { AppState, RequestKey, ClassDictionary, NotificationType } from 'src/types' |
|||
|
|||
const Login: FC = () => { |
|||
const checked = useSelector<AppState, boolean>(getChecked) |
|||
const authenticated = useSelector<AppState, boolean>(getAuthenticated) |
|||
const name = useSelector<AppState, string>(state => getFieldValue(state, 'name', '')) |
|||
const password = useSelector<AppState, string>(state => getFieldValue(state, 'password', '')) |
|||
const authenticating = useSelector<AppState, boolean>(state => getIsFetching(state, RequestKey.Authenticate)) |
|||
|
|||
const dispatch = useDispatch() |
|||
const history = useHistory() |
|||
|
|||
useEffect(() => { |
|||
if (checked && authenticated) history.push('/self') |
|||
}, [checked, authenticated]) |
|||
|
|||
useEffect(() => { |
|||
dispatch(initForm()) |
|||
dispatch(initField('name', 'id')) |
|||
dispatch(initField('password')) |
|||
}, []) |
|||
|
|||
const handleAuthenticate = async () => { |
|||
let invalid = false |
|||
|
|||
if (!name) { |
|||
dispatch(setFieldNotification('name', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (!password) { |
|||
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (invalid) return |
|||
|
|||
try { |
|||
const id = await dispatch(authenticate(name, password)) |
|||
dispatch(showNotification(NotificationType.Welcome, `Welcome back ${id}!`)) |
|||
history.push('/') |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, history) |
|||
} |
|||
} |
|||
|
|||
const buttonClassDictionary: ClassDictionary = { |
|||
button: true, |
|||
'is-primary': true, |
|||
'is-loading': authenticating, |
|||
} |
|||
|
|||
return ( |
|||
<div> |
|||
<PageHeader title="Log In to Flexor" /> |
|||
|
|||
<div className="main-content"> |
|||
<div className="centered-content"> |
|||
<div className="centered-content-icon has-background-primary"> |
|||
<span className="icon is-large has-text-white"> |
|||
<FontAwesomeIcon icon={faKey} size="2x" /> |
|||
</span> |
|||
</div> |
|||
|
|||
<TextField icon={faIdCard} name="name" label="Username" placeholder="Your Username/ID" /> |
|||
<br /> |
|||
<PasswordField placeholder="Your password" showStrength={false} /> |
|||
<br /> |
|||
|
|||
<button className={classNames(buttonClassDictionary)} onClick={() => handleAuthenticate()} disabled={authenticating}> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faKey} /> |
|||
</span> |
|||
<span>Log In</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default Login |
@ -1,56 +0,0 @@ |
|||
|
|||
import { connect } from 'react-redux' |
|||
import { handleApiError } from 'src/api/errors' |
|||
import { authenticate } from 'src/actions/authentication' |
|||
import { showNotification } from 'src/actions/notifications' |
|||
import { getChecked, getAuthenticated } from 'src/selectors/authentication' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
import { getIsFetching } from 'src/selectors/requests' |
|||
import { initForm, initField, setFieldNotification } from 'src/actions/forms' |
|||
import { AppState, AppThunkDispatch, NotificationType, RequestKey } from 'src/types' |
|||
|
|||
import Login, { Props } from './login' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
checked: getChecked(state), |
|||
authenticated: getAuthenticated(state), |
|||
name: getFieldValue(state, 'name', ''), |
|||
password: getFieldValue(state, 'password', ''), |
|||
authenticating: getIsFetching(state, RequestKey.Authenticate), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ |
|||
initForm: () => { |
|||
dispatch(initForm()) |
|||
dispatch(initField('name', 'id')) |
|||
dispatch(initField('password')) |
|||
}, |
|||
authenticate: async (name: string, password: string) => { |
|||
let invalid = false |
|||
|
|||
if (!name) { |
|||
dispatch(setFieldNotification('name', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (!password) { |
|||
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required')) |
|||
invalid = true |
|||
} |
|||
|
|||
if (invalid) return |
|||
|
|||
try { |
|||
const id = await dispatch(authenticate(name, password)) |
|||
dispatch(showNotification(NotificationType.Welcome, `Welcome back ${id}!`)) |
|||
ownProps.history.push('/') |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, ownProps.history) |
|||
} |
|||
}, |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(Login) |
@ -1,76 +0,0 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { RouteComponentProps } from 'react-router' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faIdCard, faKey } from '@fortawesome/free-solid-svg-icons' |
|||
import classNames from 'classnames' |
|||
|
|||
import PageHeader from 'src/components/page-header' |
|||
import TextField from 'src/components/forms/text-field' |
|||
import PasswordField from 'src/components/forms/password-field' |
|||
|
|||
import { ClassDictionary } from 'src/types' |
|||
|
|||
export interface Props extends RouteComponentProps { |
|||
checked: boolean |
|||
authenticated: boolean |
|||
name?: string |
|||
password?: string |
|||
authenticating?: boolean |
|||
initForm: () => void |
|||
authenticate: (name: string, password: string) => void |
|||
} |
|||
|
|||
const Login: FC<Props> = ({ |
|||
checked, |
|||
authenticated, |
|||
name = '', |
|||
password = '', |
|||
authenticating = false, |
|||
initForm, |
|||
authenticate, |
|||
history |
|||
}) => { |
|||
useEffect(() => { |
|||
if (checked && authenticated) history.push('/self') |
|||
}, [checked, authenticated]) |
|||
|
|||
useEffect(() => { |
|||
initForm() |
|||
}, []) |
|||
|
|||
const buttonClassDictionary: ClassDictionary = { |
|||
button: true, |
|||
'is-primary': true, |
|||
'is-loading': authenticating, |
|||
} |
|||
|
|||
return ( |
|||
<div> |
|||
<PageHeader title="Log In to Flexor" /> |
|||
|
|||
<div className="main-content"> |
|||
<div className="centered-content"> |
|||
<div className="centered-content-icon has-background-primary"> |
|||
<span className="icon is-large has-text-white"> |
|||
<FontAwesomeIcon icon={faKey} size="2x" /> |
|||
</span> |
|||
</div> |
|||
|
|||
<TextField icon={faIdCard} name="name" label="Username" placeholder="Your Username/ID" /> |
|||
<br /> |
|||
<PasswordField placeholder="Your password" showStrength={false} /> |
|||
<br /> |
|||
|
|||
<button className={classNames(buttonClassDictionary)} onClick={() => authenticate(name, password)} disabled={authenticating}> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faKey} /> |
|||
</span> |
|||
<span>Log In</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default Login |
@ -0,0 +1,101 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { useParams, useHistory } from 'react-router' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faUserPlus } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchGroup } from 'src/actions/groups' |
|||
import { initField } from 'src/actions/forms' |
|||
import { showNotification } from 'src/actions/notifications' |
|||
import { register } from 'src/actions/registration' |
|||
import { getEntity } from 'src/selectors/entities' |
|||
import { getForm } from 'src/selectors/forms' |
|||
|
|||
import { setTitle, valueFromForm } from 'src/utils' |
|||
import { useDeepCompareEffect } from 'src/hooks' |
|||
|
|||
import { AppState, AppThunkDispatch, Group, EntityType, NotificationType, Form } from 'src/types' |
|||
|
|||
import PageHeader from 'src/components/page-header' |
|||
import Loading from 'src/components/pages/loading' |
|||
import CreateUserForm from 'src/components/create-user-form' |
|||
|
|||
interface Params { |
|||
id: string |
|||
} |
|||
|
|||
const RegisterGroup: FC = () => { |
|||
const { id } = useParams<Params>() |
|||
const group = useSelector<AppState, Group | undefined>(state => getEntity(state, EntityType.Group, id)) |
|||
const form = useSelector<AppState, Form>(getForm) |
|||
const dispatch = useDispatch<AppThunkDispatch>() |
|||
const history = useHistory() |
|||
|
|||
useEffect(() => { |
|||
try { |
|||
dispatch(fetchGroup(id)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, history) |
|||
} |
|||
|
|||
dispatch(initField('user-id', 'id')) |
|||
dispatch(initField('user-name', 'name')) |
|||
dispatch(initField('user-email', 'email')) |
|||
dispatch(initField('password')) |
|||
dispatch(initField('user-agree')) |
|||
|
|||
setTitle('Register') |
|||
}, []) |
|||
|
|||
useDeepCompareEffect(() => { |
|||
if (group) setTitle(`Register with ${group.name}`) |
|||
}, [group]) |
|||
|
|||
const handleRegister = async () => { |
|||
if (!valueFromForm<boolean>(form, 'user-agree', false)) { |
|||
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions.')) |
|||
return |
|||
} |
|||
|
|||
try { |
|||
await dispatch(register({ |
|||
id: valueFromForm<string>(form, 'user-id', ''), |
|||
email: valueFromForm<string>(form, 'user-email', ''), |
|||
password: valueFromForm<string>(form, 'password', ''), |
|||
name: valueFromForm<string>(form, 'user-name', ''), |
|||
group: id, |
|||
})) |
|||
|
|||
dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`)) |
|||
|
|||
history.push('/self') |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, history) |
|||
} |
|||
} |
|||
|
|||
if (!group) return <Loading /> |
|||
|
|||
return ( |
|||
<div> |
|||
<PageHeader title="Register" subtitle={group.name} /> |
|||
|
|||
<div className="main-content"> |
|||
<div className="centered-content"> |
|||
<CreateUserForm /> |
|||
<br /> |
|||
|
|||
<button className="button is-primary" onClick={() => handleRegister()}> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faUserPlus} /> |
|||
</span> |
|||
<span>Create Your Account</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default RegisterGroup |
@ -1,63 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchGroup } from 'src/actions/directory' |
|||
import { initField } from 'src/actions/forms' |
|||
import { showNotification } from 'src/actions/notifications' |
|||
import { register } from 'src/actions/registration' |
|||
import { getEntity } from 'src/selectors/entities' |
|||
import { getForm } from 'src/selectors/forms' |
|||
|
|||
import { valueFromForm } from 'src/utils' |
|||
import { AppState, AppThunkDispatch, EntityType, Form, NotificationType } from 'src/types' |
|||
|
|||
import RegisterGroup, { Props } from './register-group' |
|||
|
|||
const mapStateToProps = (state: AppState, ownProps: Props) => ({ |
|||
group: getEntity(state, EntityType.Group, ownProps.match.params.id), |
|||
form: getForm(state), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: AppThunkDispatch, ownProps: Props) => ({ |
|||
initForm: () => { |
|||
dispatch(initField('user-id', 'id')) |
|||
dispatch(initField('user-name', 'name')) |
|||
dispatch(initField('user-email', 'email')) |
|||
dispatch(initField('password')) |
|||
dispatch(initField('user-agree')) |
|||
}, |
|||
fetchGroup: async () => { |
|||
try { |
|||
await dispatch(fetchGroup(ownProps.match.params.id)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, ownProps.history) |
|||
} |
|||
}, |
|||
register: async (form: Form) => { |
|||
if (!valueFromForm<boolean>(form, 'user-agree', false)) { |
|||
dispatch(showNotification(NotificationType.Error, 'You must agree to the terms and conditions.')) |
|||
return |
|||
} |
|||
|
|||
try { |
|||
await dispatch(register({ |
|||
id: valueFromForm<string>(form, 'user-id', ''), |
|||
email: valueFromForm<string>(form, 'user-email', ''), |
|||
password: valueFromForm<string>(form, 'password', ''), |
|||
name: valueFromForm<string>(form, 'user-name', ''), |
|||
group: ownProps.match.params.id, |
|||
})) |
|||
|
|||
dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`)) |
|||
|
|||
ownProps.history.push('/self') |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, ownProps.history) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(RegisterGroup) |
@ -1,61 +0,0 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { RouteComponentProps } from 'react-router' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faUserPlus } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import { setTitle } from 'src/utils' |
|||
import { useDeepCompareEffect } from 'src/hooks' |
|||
|
|||
import { Entity, Form } from 'src/types' |
|||
|
|||
import PageHeader from 'src/components/page-header' |
|||
import Loading from 'src/components/pages/loading' |
|||
import CreateUserForm from 'src/components/create-user-form' |
|||
|
|||
interface Params { |
|||
id: string |
|||
} |
|||
|
|||
export interface Props extends RouteComponentProps<Params> { |
|||
group?: Entity |
|||
form: Form |
|||
fetchGroup: () => void |
|||
initForm: () => void |
|||
register: (form: Form) => void |
|||
} |
|||
|
|||
const RegisterGroup: FC<Props> = ({ group, form, fetchGroup, initForm, register }) => { |
|||
useEffect(() => { |
|||
fetchGroup() |
|||
initForm() |
|||
setTitle('Register') |
|||
}, []) |
|||
|
|||
useDeepCompareEffect(() => { |
|||
if (group) setTitle(`Register @ ${group.name}`) |
|||
}, [group]) |
|||
|
|||
if (!group) return <Loading /> |
|||
|
|||
return ( |
|||
<div> |
|||
<PageHeader title="Register" subtitle={group ? group.name : ''} /> |
|||
|
|||
<div className="main-content"> |
|||
<div className="centered-content"> |
|||
<CreateUserForm /> |
|||
<br /> |
|||
|
|||
<button className="button is-primary" onClick={() => register(form)}> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faUserPlus} /> |
|||
</span> |
|||
<span>Create Your Account</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default RegisterGroup |
@ -1,52 +0,0 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { RouteComponentProps } from 'react-router' |
|||
|
|||
import PageHeader from 'src/components/page-header' |
|||
import CreateGroupStep from 'src/components/create-group-step' |
|||
import CreateUserStep from 'src/components/create-user-step' |
|||
|
|||
import { setTitle } from 'src/utils' |
|||
import { Form } from 'src/types' |
|||
|
|||
export interface Props extends RouteComponentProps { |
|||
stepIndex: number |
|||
form: Form |
|||
initForm: () => void |
|||
register: (form: Form) => void |
|||
} |
|||
|
|||
const Register: FC<Props> = ({ stepIndex, form, initForm, register }) => { |
|||
const title = () => { |
|||
switch (stepIndex) { |
|||
case 0: return 'Create Your Account' |
|||
default: return 'Create a Community' |
|||
} |
|||
} |
|||
|
|||
const component = () => { |
|||
switch (stepIndex) { |
|||
case 0: return <CreateUserStep /> |
|||
default: return <CreateGroupStep register={() => register(form)} /> |
|||
} |
|||
} |
|||
|
|||
useEffect(() => { |
|||
initForm() |
|||
}, []) |
|||
|
|||
useEffect(() => { |
|||
setTitle(title()) |
|||
}, [stepIndex]) |
|||
|
|||
return ( |
|||
<div> |
|||
<PageHeader title={title()} /> |
|||
|
|||
<div className="main-content"> |
|||
{component()} |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default Register |
@ -1,35 +0,0 @@ |
|||
import { Dispatch } from 'redux' |
|||
import { connect } from 'react-redux' |
|||
import { unauthenticate } from 'src/actions/authentication' |
|||
import { getAuthenticated, getChecked, getAuthenticatedUser } from 'src/selectors/authentication' |
|||
import { AppState, User } from 'src/types' |
|||
|
|||
import Self from './self' |
|||
import { initForm, initField, setFieldValue } from 'src/actions/forms' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
checked: getChecked(state), |
|||
authenticated: getAuthenticated(state), |
|||
user: getAuthenticatedUser(state), |
|||
}) |
|||
|
|||
const mapDispatchToProps = (dispatch: Dispatch) => ({ |
|||
initForm: (user: User) => { |
|||
dispatch(initForm()) |
|||
dispatch(initField('name')) |
|||
dispatch(initField('about')) |
|||
dispatch(setFieldValue('name', user.name as string)) |
|||
dispatch(setFieldValue('about', user.about as string)) |
|||
}, |
|||
logout: async () => { |
|||
localStorage.clear() |
|||
dispatch(unauthenticate()) |
|||
|
|||
window.location.href = '/' |
|||
}, |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps, |
|||
mapDispatchToProps |
|||
)(Self) |
@ -1,7 +1,5 @@ |
|||
import React, { FC } from 'react' |
|||
|
|||
import './spinner.scss' |
|||
|
|||
const Spinner: FC = () => ( |
|||
<div className="sk-cube-grid"> |
|||
<div className="sk-cube sk-cube1"></div> |
@ -0,0 +1,25 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { Link } from 'react-router-dom' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import { setTitle } from 'src/utils' |
|||
|
|||
const UserApps: FC = () => { |
|||
useEffect(() => { |
|||
setTitle('Your Apps') |
|||
}, []) |
|||
|
|||
return ( |
|||
<div> |
|||
<Link className="button is-primary" to="/apps/new"> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faPlusCircle} /> |
|||
</span> |
|||
<span>Create a new App</span> |
|||
</Link> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default UserApps |
@ -1,15 +0,0 @@ |
|||
import { connect } from 'react-redux' |
|||
|
|||
import { getAuthenticated, getAuthenticatedUser } from 'src/selectors/authentication' |
|||
import { AppState } from 'src/types' |
|||
|
|||
import UserInfo from './user-info' |
|||
|
|||
const mapStateToProps = (state: AppState) => ({ |
|||
authenticated: getAuthenticated(state), |
|||
user: getAuthenticatedUser(state), |
|||
}) |
|||
|
|||
export default connect( |
|||
mapStateToProps |
|||
)(UserInfo) |
@ -1,3 +0,0 @@ |
|||
article#user-info { |
|||
padding: 20px; |
|||
} |
@ -0,0 +1,18 @@ |
|||
|
|||
import { createSelector } from 'reselect' |
|||
|
|||
import { denormalize } from 'src/utils/normalization' |
|||
import { getEntityStore } from './entities' |
|||
import { getAuthenticatedUserId } from './authentication' |
|||
|
|||
import { EntityType, App } from 'src/types' |
|||
|
|||
export const getSelfApps = createSelector( |
|||
[getEntityStore, getAuthenticatedUserId], |
|||
(entities, userId) => { |
|||
const apps = entities[EntityType.App] |
|||
if (!apps) return [] |
|||
|
|||
return denormalize(Object.values(apps).filter(app => app.userId === userId).map(user => user.id), EntityType.User, entities) as App[] |
|||
} |
|||
) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue