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 { Link } from 'react-router-dom' |
||||
import { faEnvelope, faIdCard } from '@fortawesome/free-solid-svg-icons' |
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 ( |
return ( |
||||
<div className="container"> |
<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 /> |
<br /> |
||||
<TextField name="user-name" label="Display Name" placeholder="Whatever you want to go by" /> |
<TextField name="user-name" label="Display Name" placeholder="Whatever you want to go by" /> |
||||
<br /> |
<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 React, { FC, useEffect } from 'react' |
||||
|
import { useSelector, useDispatch } from 'react-redux' |
||||
import { Link } from 'react-router-dom' |
import { Link } from 'react-router-dom' |
||||
import noop from 'lodash/noop' |
|
||||
import moment from 'moment' |
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 |
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(() => { |
useEffect(() => { |
||||
if (logs.length === 0) fetchLogs() |
|
||||
}, [group, fetchLogs]) |
|
||||
|
if (logs.length === 0) { |
||||
|
try { |
||||
|
dispatch(fetchLogs(group)) |
||||
|
} catch (err) { |
||||
|
handleApiError(err, dispatch) |
||||
|
} |
||||
|
} |
||||
|
}, [group]) |
||||
|
|
||||
return ( |
return ( |
||||
<table className="table is-bordered is-striped is-narrow is-hoverable is-fullwidth"> |
<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 React, { FC } from 'react' |
||||
import { Notification as INotification } from 'src/types' |
|
||||
|
import { useSelector, useDispatch } from 'react-redux' |
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
||||
import { faDoorOpen } from '@fortawesome/free-solid-svg-icons' |
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 ( |
return ( |
||||
<div id="notification-container"> |
<div id="notification-container"> |
||||
{notifications.map(notification => { |
{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 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
||||
import { faEdit, faUserCheck } from '@fortawesome/free-solid-svg-icons' |
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 { 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 PageHeader from 'src/components/page-header' |
||||
import GroupInfo from 'src/components/group-info' |
import GroupInfo from 'src/components/group-info' |
||||
|
import Loading from 'src/components/pages/loading' |
||||
|
|
||||
interface Params { |
interface Params { |
||||
id: string |
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(() => { |
useEffect(() => { |
||||
fetchGroup() |
|
||||
|
try { |
||||
|
dispatch(fetchGroup(id)) |
||||
|
} catch (err) { |
||||
|
handleApiError(err, dispatch, history) |
||||
|
} |
||||
}, []) |
}, []) |
||||
|
|
||||
useEffect(() => { |
useEffect(() => { |
||||
if (group) setTitle(group.name) |
if (group) setTitle(group.name) |
||||
}, [group]) |
}, [group]) |
||||
|
|
||||
if (!group) { |
|
||||
return ( |
|
||||
<div> |
|
||||
<PageHeader title="Group" /> |
|
||||
<div className="main-content"></div> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
|
if (!group) return <Loading /> |
||||
|
|
||||
const isAdmin = group.membership === GroupMembershipType.Admin |
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 React, { FC } from 'react' |
||||
|
|
||||
import PageHeader from 'src/components/page-header' |
import PageHeader from 'src/components/page-header' |
||||
|
import Spinner from 'src/components/spinner' |
||||
|
|
||||
const Loading: FC = () => ( |
const Loading: FC = () => ( |
||||
<div> |
<div> |
||||
<PageHeader title="Loading..." /> |
<PageHeader title="Loading..." /> |
||||
<div className="main-content"></div> |
|
||||
|
<div className="main-content"> |
||||
|
<Spinner /> |
||||
|
</div> |
||||
</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 React, { FC } from 'react' |
||||
|
|
||||
import './spinner.scss' |
|
||||
|
|
||||
const Spinner: FC = () => ( |
const Spinner: FC = () => ( |
||||
<div className="sk-cube-grid"> |
<div className="sk-cube-grid"> |
||||
<div className="sk-cube sk-cube1"></div> |
<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