Dwayne Harris 5 years ago
parent
commit
657e11ff55
  1. 28
      src/actions/authentication.ts
  2. 4
      src/actions/forms.ts
  3. 6
      src/actions/registration.ts
  4. 9
      src/api/errors.ts
  5. 20
      src/components/app.tsx
  6. 10
      src/components/create-user-form.tsx
  7. 34
      src/components/pages/create-app.tsx
  8. 3
      src/components/pages/developers.tsx
  9. 7
      src/components/pages/group-admin.tsx
  10. 4
      src/components/pages/login.tsx
  11. 12
      src/components/pages/register-group.tsx
  12. 22
      src/components/pages/register.tsx
  13. 50
      src/components/pages/self.tsx
  14. 2
      src/components/user-info.tsx
  15. 7
      src/constants/index.ts
  16. 3
      src/reducers/forms.ts
  17. 2
      src/styles/app.scss
  18. 3
      src/types/entities.ts
  19. 1
      src/types/store.ts
  20. 3
      src/utils/normalization.ts

28
src/actions/authentication.ts

@ -106,3 +106,31 @@ export const authenticate = (name: string, password: string): AppThunkAction<str
throw err
}
}
export const updateSelf = (name: string, about: string, requiresApproval: boolean, privacy: string): AppThunkAction => async dispatch => {
dispatch(startRequest(RequestKey.UpdateSelf))
try {
const self = await apiFetch<Entity>({
path: '/api/self',
method: 'put',
body: {
name,
about,
requiresApproval,
privacy,
},
})
const result = normalize([self], EntityType.User)
dispatch(setEntities(result.entities))
dispatch(setUser(self.id))
dispatch(setAuthenticated(true))
dispatch(finishRequest(RequestKey.UpdateSelf, true))
} catch (err) {
dispatch(finishRequest(RequestKey.UpdateSelf, false))
throw err
}
}

4
src/actions/forms.ts

@ -9,6 +9,7 @@ export interface InitFieldAction extends Action {
type: 'FORMS_INIT_FIELD'
payload: {
name: string
value: FormValue
apiName?: string
}
}
@ -44,10 +45,11 @@ export const initForm = (): InitFormAction => ({
type: 'FORMS_INIT',
})
export const initField = (name: string, apiName?: string): InitFieldAction => ({
export const initField = (name: string, value: FormValue, apiName?: string): InitFieldAction => ({
type: 'FORMS_INIT_FIELD',
payload: {
name,
value,
apiName,
},
})

6
src/actions/registration.ts

@ -118,6 +118,8 @@ interface RegisterOptions {
email: string
password: string
name?: string
requiresApproval: boolean
privacy: string
group?: string
}
@ -128,7 +130,7 @@ interface RegisterResponse {
}
export const register = (options: RegisterOptions): AppThunkAction<string> => async dispatch => {
const { id, email, password, name, group } = options
const { id, email, password, name, requiresApproval, privacy, group } = options
dispatch(startRequest(RequestKey.Register))
@ -141,6 +143,8 @@ export const register = (options: RegisterOptions): AppThunkAction<string> => as
email,
password,
name,
requiresApproval,
privacy,
group,
},
})

9
src/api/errors.ts

@ -5,8 +5,11 @@ import { showNotification } from 'src/actions/notifications'
import { AppThunkDispatch, FormNotification, NotificationType } from 'src/types'
export function handleApiError(err: HttpError, dispatch: AppThunkDispatch, history?: History) {
console.error('Error:', err)
if (err instanceof ServerError) {
dispatch(showNotification(NotificationType.Error, 'Server Error'))
return
}
if (err instanceof BadRequestError) {
@ -16,16 +19,22 @@ export function handleApiError(err: HttpError, dispatch: AppThunkDispatch, histo
const { field, type, message } = error
if (field) dispatch(setFieldNotification(field, type, message))
}
return
}
if (err instanceof UnauthorizedError) {
dispatch(showNotification(NotificationType.Error, 'You need to be logged in.'))
if (history) history.push('/login')
return
}
if (err instanceof NotFoundError) {
dispatch(showNotification(NotificationType.Error, 'Not found.'))
return
}
dispatch(showNotification(NotificationType.Error, `Error: ${err.message}`))
}
export class HttpError extends Error {

20
src/components/app.tsx

@ -1,7 +1,8 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom'
import { BrowserRouter as Router, Route, Switch, Link, useHistory } from 'react-router-dom'
import { handleApiError } from 'src/api/errors'
import { fetchSelf, setChecked } from 'src/actions/authentication'
import { getFetching } from 'src/selectors'
import { getCollapsed } from 'src/selectors/menu'
@ -16,6 +17,7 @@ import Spinner from './spinner'
import UserInfo from './user-info'
import About from './pages/about'
import CreateApp from './pages/create-app'
import Developers from './pages/developers'
import Group from './pages/group'
import GroupAdmin from './pages/group-admin'
@ -38,12 +40,21 @@ const App: FC = () => {
const mainMenuWidth = 275
const mainColumnMargin = collapsed ? 0 : mainMenuWidth
useEffect(() => {
const init = async () => {
if (localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)) {
dispatch(fetchSelf())
try {
await dispatch(fetchSelf())
} catch (err) {
console.log('err', err)
handleApiError(err, dispatch)
}
} else {
dispatch(setChecked())
}
}
useEffect(() => {
init()
}, [])
return (
@ -85,6 +96,9 @@ const App: FC = () => {
<Route path="/self/:tab?">
<Self />
</Route>
<Route path="/developers/create">
<CreateApp />
</Route>
<Route path="/developers">
<Developers />
</Route>

10
src/components/create-user-form.tsx

@ -1,12 +1,14 @@
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 { faEnvelope, faIdCard, faUserShield } from '@fortawesome/free-solid-svg-icons'
import { checkUserAvailability } from 'src/actions/registration'
import { PRIVACY_OPTIONS } from 'src/constants'
import CheckboxField from './forms/checkbox-field'
import TextField from './forms/text-field'
import PasswordField from './forms/password-field'
import SelectField from './forms/select-field'
const CreateUserForm: FC = () => {
const dispatch = useDispatch()
@ -27,6 +29,12 @@ const CreateUserForm: FC = () => {
<br />
<PasswordField placeholder="Your new password" />
<br />
<SelectField name="user-privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<br />
<CheckboxField name="user-requires-approval">
You must approve each Subscription request from other users.
</CheckboxField>
<br />
<CheckboxField name="user-agree">
I agree to the User <Link to="/terms">terms and conditions</Link>.
</CheckboxField>

34
src/components/pages/create-app.tsx

@ -1,12 +1,28 @@
import React, { FC, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons'
import { initForm, initField } from 'src/actions/forms'
import { setTitle } from 'src/utils'
import PageHeader from 'src/components/page-header'
import TextField from 'src/components/forms/text-field'
import TextareaField from 'src/components/forms/textarea-field'
const CreateApp: FC = () => {
const dispatch = useDispatch()
useEffect(() => {
setTitle('Create a new App')
})
dispatch(initForm())
dispatch(initField('name', ''))
dispatch(initField('about', ''))
dispatch(initField('websiteUrl', ''))
dispatch(initField('companyName', ''))
dispatch(initField('version', ''))
}, [])
return (
<div>
@ -14,7 +30,23 @@ const CreateApp: FC = () => {
<div className="main-content">
<div className="centered-content">
<TextField name="name" label="Name" />
<br />
<TextareaField name="about" label="About" placeholder="Description of this app" />
<br />
<TextField name="websiteUrl" label="Website" placeholder="Website URL (optional)" />
<br />
<TextField name="companyName" label="Company" placeholder="Your company or organization (optional)" />
<br />
<TextField name="version" label="Version" placeholder="Current Version of the app (ex: 0.0.1beta5)" />
<br />
<button className="button is-success">
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>
<span>Create</span>
</button>
</div>
</div>
</div>

3
src/components/pages/developers.tsx

@ -27,8 +27,9 @@ const Developers: FC = () => {
<div className="main-content">
<div className="centered-content">
<p>This is where you manage apps you create.</p>
<br />
<Link className="button is-primary" to="/apps/new">
<Link className="button is-primary" to="/developers/create">
<span className="icon is-small">
<FontAwesomeIcon icon={faPlusCircle} />
</span>

7
src/components/pages/group-admin.tsx

@ -63,10 +63,9 @@ const GroupAdmin: FC = () => {
}
dispatch(initForm())
dispatch(initField('about'))
dispatch(initField('expiration'))
dispatch(initField('limit'))
dispatch(setFieldValue('about', group.about as string))
dispatch(initField('about', group.about))
dispatch(initField('expiration', '0'))
dispatch(initField('limit', '0'))
setTitle(`${group.name} Administration`)
}

4
src/components/pages/login.tsx

@ -35,8 +35,8 @@ const Login: FC = () => {
useEffect(() => {
dispatch(initForm())
dispatch(initField('name', 'id'))
dispatch(initField('password'))
dispatch(initField('name', '', 'id'))
dispatch(initField('password', ''))
}, [])
const handleAuthenticate = async () => {

12
src/components/pages/register-group.tsx

@ -39,11 +39,11 @@ const RegisterGroup: FC = () => {
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'))
dispatch(initField('user-id', '', 'id'))
dispatch(initField('user-name', '', 'name'))
dispatch(initField('user-email', '', 'email'))
dispatch(initField('password', ''))
dispatch(initField('user-agree', false))
setTitle('Register')
}, [])
@ -64,6 +64,8 @@ const RegisterGroup: FC = () => {
email: valueFromForm<string>(form, 'user-email', ''),
password: valueFromForm<string>(form, 'password', ''),
name: valueFromForm<string>(form, 'user-name', ''),
requiresApproval: valueFromForm<boolean>(form, 'user-requires-approval', false),
privacy: valueFromForm<string>(form, 'user-privacy', 'public'),
group: id,
}))

22
src/components/pages/register.tsx

@ -38,6 +38,8 @@ const Register: FC = () => {
email: valueFromForm<string>(form, 'user-email', ''),
password: valueFromForm<string>(form, 'password', ''),
name: valueFromForm<string>(form, 'user-name', ''),
requiresApproval: valueFromForm<boolean>(form, 'user-requires-approval', true),
privacy: valueFromForm<string>(form, 'user-privacy', 'open')
}))
await dispatch(createGroup({
@ -69,16 +71,16 @@ const Register: FC = () => {
useEffect(() => {
dispatch(initForm())
dispatch(initField('group-name', 'name'))
dispatch(initField('group-registration', 'registration'))
dispatch(initField('group-agree'))
dispatch(initField('user-id', 'id'))
dispatch(initField('user-name', 'name'))
dispatch(initField('user-email', 'email'))
dispatch(initField('password'))
dispatch(initField('user-agree'))
dispatch(setFieldValue('group-registration', 'open'))
dispatch(initField('group-name', '', 'name'))
dispatch(initField('group-registration', 'open', 'registration'))
dispatch(initField('group-agree', false))
dispatch(initField('user-id', '', 'id'))
dispatch(initField('user-name', '', 'name'))
dispatch(initField('user-email', '', 'email'))
dispatch(initField('password', ''))
dispatch(initField('user-requires-approval', true, 'requiresApproval'))
dispatch(initField('user-privacy', 'public', 'privacy'))
dispatch(initField('user-agree', false))
}, [])
useEffect(() => {

50
src/components/pages/self.tsx

@ -3,20 +3,25 @@ import { useSelector, useDispatch } from 'react-redux'
import { Link, useParams, useHistory } from 'react-router-dom'
import moment from 'moment'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faDoorOpen, faCheckCircle, faPlusCircle, faIdCard, faEnvelope } from '@fortawesome/free-solid-svg-icons'
import { faDoorOpen, faCheckCircle, faPlusCircle, faIdCard, faEnvelope, faUserShield } from '@fortawesome/free-solid-svg-icons'
import { unauthenticate } from 'src/actions/authentication'
import { initForm, initField, setFieldValue } from 'src/actions/forms'
import { unauthenticate, updateSelf } from 'src/actions/authentication'
import { initForm, initField } from 'src/actions/forms'
import { getAuthenticated, getChecked, getAuthenticatedUser } from 'src/selectors/authentication'
import { getForm } from 'src/selectors/forms'
import { handleApiError } from 'src/api/errors'
import { PRIVACY_OPTIONS } from 'src/constants'
import { useAuthenticationCheck, useDeepCompareEffect } from 'src/hooks'
import { setTitle } from 'src/utils'
import { AppState, User, Tab } from 'src/types'
import { setTitle, valueFromForm } from 'src/utils'
import { AppState, User, Tab, Form } from 'src/types'
import PageHeader from 'src/components/page-header'
import Loading from 'src/components/pages/loading'
import TextField from 'src/components/forms/text-field'
import TextareaField from 'src/components/forms/textarea-field'
import SelectField from 'src/components/forms/select-field'
import CheckboxField from 'src/components/forms/checkbox-field'
interface Params {
tab: string
@ -36,6 +41,7 @@ const Self: FC = () => {
const checked = useSelector<AppState, boolean>(getChecked)
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const form = useSelector<AppState, Form>(getForm)
useAuthenticationCheck(checked, authenticated, history)
@ -45,15 +51,28 @@ const Self: FC = () => {
window.location.href = '/'
}
const handleUpdate = () => {
const name = valueFromForm<string>(form, 'name', '')
const about = valueFromForm<string>(form, 'about', '')
const requiresApproval = valueFromForm<boolean>(form, 'requiresApproval', true)
const privacy = valueFromForm<string>(form, 'privacy', 'public')
try {
dispatch(updateSelf(name, about, requiresApproval, privacy))
} catch (err) {
handleApiError(err, dispatch, history)
}
}
useDeepCompareEffect(() => {
if (user) {
setTitle(`${user.name} (@${user.id})`)
dispatch(initForm())
dispatch(initField('name'))
dispatch(initField('about'))
dispatch(setFieldValue('name', user.name as string))
dispatch(setFieldValue('about', user.about as string))
dispatch(initField('name', user.name))
dispatch(initField('about', user.about || ''))
dispatch(initField('requiresApproval', user.requiresApproval))
dispatch(initField('privacy', user.privacy))
}
}, [user])
@ -117,11 +136,16 @@ const Self: FC = () => {
<TextField name="name" label="Name" placeholder="Your Display Name" />
<br />
<TextareaField name="about" label="About" placeholder="Your Bio" />
<br />
<SelectField name="privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<br />
<CheckboxField name="requiresApproval">
You must approve each Subscription request from other users.
</CheckboxField>
<br /><br />
<button className="button is-primary">
<button className="button is-primary" onClick={() => handleUpdate()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>
@ -145,12 +169,12 @@ const Self: FC = () => {
<br />
<button className="button is-primary">
<Link to="/developers/create" className="button is-primary">
<span className="icon is-small">
<FontAwesomeIcon icon={faPlusCircle} />
</span>
<span>Create a new App</span>
</button>
</Link>
</div>
}
</div>

2
src/components/user-info.tsx

@ -5,10 +5,8 @@ import { Link } from 'react-router-dom'
import { getAuthenticatedUser } from 'src/selectors/authentication'
import { AppState, User } from 'src/types'
const UserInfo: FC = () => {
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const hasAvatar = user && user.imageUrl
const imageUrl = hasAvatar ? user!.imageUrl : undefined

7
src/constants/index.ts

@ -4,3 +4,10 @@ export const MAX_NAME_LENGTH = 80
export const LOCAL_STORAGE_ACCESS_TOKEN_KEY = 'FLEXOR_ACCESS_TOKEN'
export const LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY = 'FLEXOR_ACCESS_TOKEN_AT'
export const LOCAL_STORAGE_REFRESH_TOKEN_KEY = 'FLEXOR_REFRESH_TOKEN'
export const PRIVACY_OPTIONS = {
public: 'Anyone can see your posts',
group: 'Only the people in your community can see your posts',
subscribers: 'Only your subscribers can see your posts',
private: 'Nobody can see your posts',
}

3
src/reducers/forms.ts

@ -17,7 +17,7 @@ const reducer: Reducer<FormsState, FormsActions> = (state = initialState, action
notification: undefined,
}
case 'FORMS_INIT_FIELD':
const { name, apiName } = action.payload
const { name, value, apiName } = action.payload
return {
...state,
@ -25,6 +25,7 @@ const reducer: Reducer<FormsState, FormsActions> = (state = initialState, action
...state.form,
[name]: {
name,
value,
apiName,
},
},

2
src/styles/app.scss

@ -40,7 +40,7 @@ div#main-menu {
bottom: 0;
display: flex;
flex-direction: column;
position: absolute;
position: fixed;
right: 0;
top: 0;
}

3
src/types/entities.ts

@ -21,6 +21,7 @@ export interface Entity {
export type Group = Entity & {
name: string
membership?: GroupMembershipType
about: string
}
export type Installation = {
@ -35,6 +36,8 @@ export type User = Entity & {
about?: string
imageUrl?: string
coverImageUrl?: string
requiresApproval: boolean
privacy: string
installations: Installation[]
}

1
src/types/store.ts

@ -23,6 +23,7 @@ export enum RequestKey {
FetchInvitations = 'fetch_invitations',
FetchApps = 'fetch_apps',
FetchSelfApps = 'fetch_self_apps',
UpdateSelf = 'update_self',
}
export type FormValue = string | number | boolean

3
src/utils/normalization.ts

@ -53,11 +53,12 @@ export function normalize(entities: Entity[], type: EntityType): NormalizeResult
case EntityType.User:
keys = entities.map(entity => {
const user = entity as User
const { installations = [] } = user
return set(type, newStore, {
...user,
group: set(EntityType.Group, newStore, user.group),
installations: user.installations.map(installation => {
installations: installations.map(installation => {
return {
...installation,
app: set(EntityType.App, newStore, installation.app),

Loading…
Cancel
Save