Dwayne Harris 5 years ago
parent
commit
64436ec719
  1. 87
      src/actions/apps.ts
  2. 7
      src/actions/registration.ts
  3. 4
      src/components/app.tsx
  4. 2
      src/components/create-group-form.tsx
  5. 21
      src/components/create-group-step.tsx
  6. 35
      src/components/create-user-step.tsx
  7. 84
      src/components/pages/create-app.tsx
  8. 6
      src/components/pages/developers.tsx
  9. 91
      src/components/pages/view-app.tsx
  10. 6
      src/selectors/entities.ts
  11. 2
      src/styles/app.scss
  12. 3
      src/types/entities.ts
  13. 5
      src/types/index.ts
  14. 21
      src/types/store.ts

87
src/actions/apps.ts

@ -1,10 +1,11 @@
import { apiFetch } from 'src/api'
import { setEntities } from 'src/actions/entities'
import { startRequest, finishRequest } from 'src/actions/requests'
import { setFieldNotification } from 'src/actions/forms'
import { objectToQuerystring } from 'src/utils'
import { normalize } from 'src/utils/normalization'
import { AppThunkAction, RequestKey, EntityType, App }from 'src/types'
import { AppThunkAction, RequestKey, EntityType, App, AvailabilityResponse, NotificationType, AppThunkDispatch }from 'src/types'
interface AppsResponse {
apps: App[]
@ -46,3 +47,87 @@ export const fetchSelfApps = (sort?: string): AppThunkAction => async dispatch =
throw err
}
}
export const checkAppAvailability = (name: string): AppThunkAction => async dispatch => {
dispatch(startRequest(RequestKey.FetchAppAvailability))
try {
const { id, available } = await apiFetch<AvailabilityResponse>({
path: '/api/app/available',
method: 'post',
body: {
name,
},
})
if (available) {
dispatch(setFieldNotification('name', NotificationType.Success, `${id} is available`))
} else {
dispatch(setFieldNotification('name', NotificationType.Error, `${id} isn't available`))
}
dispatch(finishRequest(RequestKey.FetchAppAvailability, true))
} catch (err) {
dispatch(finishRequest(RequestKey.FetchAppAvailability, false))
throw err
}
}
interface CreateAppOptions {
name: string
about?: string
websiteUrl?: string
companyName?: string
version: string
composerUrl?: string
rendererUrl?: string
}
interface CreateAppResponse {
id: string
}
export const createApp = (options: CreateAppOptions): AppThunkAction<string> => async dispatch => {
const { name, about, websiteUrl, companyName, version, composerUrl, rendererUrl } = options
dispatch(startRequest(RequestKey.CreateApp))
try {
const { id } = await apiFetch<CreateAppResponse>({
path: '/api/app',
method: 'post',
body: {
name,
about,
websiteUrl,
companyName,
version,
composerUrl,
rendererUrl,
},
})
dispatch(finishRequest(RequestKey.CreateApp, true))
return id
} catch (err) {
dispatch(finishRequest(RequestKey.CreateApp, false))
throw err
}
}
export const fetchApp = (id: string): AppThunkAction => async dispatch => {
dispatch(startRequest(RequestKey.FetchApp))
try {
const app = await apiFetch<App>({
path: `/api/app/${id}`,
method: 'get',
})
const apps = normalize([app], EntityType.App)
dispatch(setEntities(apps.entities))
dispatch(finishRequest(RequestKey.FetchApp, true))
} catch (err) {
dispatch(finishRequest(RequestKey.FetchApp, false))
throw err
}
}

7
src/actions/registration.ts

@ -9,7 +9,7 @@ import {
LOCAL_STORAGE_REFRESH_TOKEN_KEY,
} from 'src/constants'
import { AppThunkAction, NotificationType, RequestKey } from 'src/types'
import { AppThunkAction, NotificationType, RequestKey, AvailabilityResponse } from 'src/types'
export interface SetStepAction extends Action {
type: 'REGISTRATION_SET_STEP'
@ -18,11 +18,6 @@ export interface SetStepAction extends Action {
export type RegistrationActions = SetStepAction
interface AvailabilityResponse {
id: string
available: boolean
}
export const setStep = (step: number): SetStepAction => ({
type: 'REGISTRATION_SET_STEP',
payload: step,

4
src/components/app.tsx

@ -27,6 +27,7 @@ import Login from './pages/login'
import Register from './pages/register'
import RegisterGroup from './pages/register-group'
import Self from './pages/self'
import ViewApp from './pages/view-app'
import '../styles/app.scss'
import '../styles/spinner.scss'
@ -84,6 +85,9 @@ const App: FC = () => {
<Route path="/c/:id">
<Group />
</Route>
<Route path="/a/:id">
<ViewApp />
</Route>
<Route path="/login">
<Login />
</Route>

2
src/components/create-group-form.tsx

@ -53,7 +53,7 @@ const CreateGroupForm: FC = () => {
<br />
<CheckboxField name="group-agree">
I agree to the Community <Link to="/terms">terms and conditions</Link>.
I agree to the Communities <Link to="/terms/communities">terms and conditions</Link>.
</CheckboxField>
</div>
)

21
src/components/create-group-step.tsx

@ -6,33 +6,34 @@ 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 { getForm, getFieldValue } from 'src/selectors/forms'
import { MAX_ID_LENGTH } from 'src/constants'
import { AppState, AppThunkDispatch, NotificationType } from 'src/types'
import { AppState, AppThunkDispatch, NotificationType, Form } from 'src/types'
import CreateGroupForm from './create-group-form'
import { valueFromForm } from 'src/utils'
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 form = useSelector<AppState, Form>(getForm)
const dispatch = useDispatch<AppThunkDispatch>()
const next = (name: string, registration: string, agree: boolean) => {
const next = () => {
let invalid = false
if (!name) {
const name = valueFromForm<string>(form, 'group-name')
const agree = valueFromForm<boolean>(form, 'group-agree')
if (!name || name === '') {
dispatch(setFieldNotification('group-name', NotificationType.Error, 'This is required'))
invalid = true
}
if (name.length > MAX_ID_LENGTH) {
if (name && name.length > MAX_ID_LENGTH) {
dispatch(setFieldNotification('group-name', NotificationType.Error, `This must be less than ${MAX_ID_LENGTH} characters`))
invalid = true
}
@ -72,7 +73,7 @@ const CreateGroupStep: FC<Props> = ({ register }) => {
<div className="level-right">
<p className="level-item">
<button className="button is-success" onClick={() => next(name, registration, agree)}>Finish</button>
<button className="button is-success" onClick={() => next()}>Finish</button>
</p>
</div>
</nav>

35
src/components/create-user-step.tsx

@ -7,40 +7,43 @@ 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 { getForm, getFieldValue } from 'src/selectors/forms'
import { MAX_ID_LENGTH, MAX_NAME_LENGTH } from 'src/constants'
import { AppState, AppThunkDispatch, NotificationType } from 'src/types'
import { valueFromForm } from 'src/utils'
import { AppState, AppThunkDispatch, NotificationType, Form } 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 form = useSelector<AppState, Form>(getForm)
const dispatch = useDispatch<AppThunkDispatch>()
const next = (userId: string, name: string, email: string, password: string, agree: boolean) => {
const next = () => {
let invalid = false
if (!userId) {
const userId = valueFromForm<string>(form, 'user-id')
const name = valueFromForm<string>(form, 'user-name')
const email = valueFromForm<string>(form, 'user-email')
const password = valueFromForm<string>(form, 'password')
const agree = valueFromForm<boolean>(form, 'agree')
if (!userId || userId === '') {
dispatch(setFieldNotification('user-id', NotificationType.Error, 'This is required'))
invalid = true
}
if (userId.length > MAX_ID_LENGTH) {
if (userId && 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) {
if (name && name.length > MAX_NAME_LENGTH) {
dispatch(setFieldNotification('user-name', NotificationType.Error, `This must be less than ${MAX_NAME_LENGTH} characters`))
invalid = true
}
if (email === '') {
if (!email || email === '') {
dispatch(setFieldNotification('user-email', NotificationType.Error, 'This is required'))
invalid = true
}
@ -51,7 +54,7 @@ const CreateUserStep: FC = () => {
invalid = true
}
if (password === '') {
if (!password || password === '') {
dispatch(setFieldNotification('password', NotificationType.Error, 'This is required'))
invalid = true
} else {
@ -85,7 +88,7 @@ const CreateUserStep: FC = () => {
<div className="level-right">
<p className="level-item">
<button className="button is-success" onClick={() => next(userId, name, email, password, agree)}>
<button className="button is-success" onClick={() => next()}>
<span>Community</span>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowRight} />

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

@ -1,17 +1,74 @@
import React, { FC, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { Link, useHistory } from 'react-router-dom'
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 { checkAppAvailability, createApp } from 'src/actions/apps'
import { initForm, initField, setFieldNotification } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { getForm } from 'src/selectors/forms'
import { setTitle, valueFromForm } from 'src/utils'
import { AppState, Form, NotificationType, AppThunkDispatch } from 'src/types'
import PageHeader from 'src/components/page-header'
import TextField from 'src/components/forms/text-field'
import TextareaField from 'src/components/forms/textarea-field'
import CheckboxField from 'src/components/forms/checkbox-field'
const CreateApp: FC = () => {
const dispatch = useDispatch()
const form = useSelector<AppState, Form>(getForm)
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
const checkAvailability = (value: string) => {
if (value.length > 3) {
dispatch(checkAppAvailability(value))
}
}
const handleCreate = async () => {
let invalid = false
const name = valueFromForm<string>(form, 'name')
const about = valueFromForm<string>(form, 'about')
const websiteUrl = valueFromForm<string>(form, 'websiteUrl')
const companyName = valueFromForm<string>(form, 'companyName')
const version = valueFromForm<string>(form, 'version')
const composerUrl = valueFromForm<string>(form, 'composerUrl')
const rendererUrl = valueFromForm<string>(form, 'rendererUrl')
const agree = valueFromForm<boolean>(form, 'agree')
if (!name || name === '') {
dispatch(setFieldNotification('name', NotificationType.Error, 'This is required'))
invalid = true
}
if (!version || version === '') {
dispatch(setFieldNotification('version', NotificationType.Error, 'This is required'))
invalid = true
}
if (!agree) {
dispatch(setFieldNotification('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
const id = await dispatch(createApp({
name: name!,
version: version!,
about,
websiteUrl,
companyName,
composerUrl,
rendererUrl,
}))
history.push(`/a/${id}`)
}
useEffect(() => {
setTitle('Create a new App')
@ -22,6 +79,9 @@ const CreateApp: FC = () => {
dispatch(initField('websiteUrl', ''))
dispatch(initField('companyName', ''))
dispatch(initField('version', ''))
dispatch(initField('composerUrl', ''))
dispatch(initField('rendererUrl', ''))
dispatch(initField('agree', false))
}, [])
return (
@ -29,8 +89,8 @@ const CreateApp: FC = () => {
<PageHeader title="Create a new App" />
<div className="main-content">
<div className="centered-content">
<TextField name="name" label="Name" />
<div className="centered-content-narrow">
<TextField name="name" label="Name" placeholder="App ID/Name" onBlur={e => checkAvailability(e.target.value)} />
<br />
<TextareaField name="about" label="About" placeholder="Description of this app" />
<br />
@ -39,9 +99,19 @@ const CreateApp: FC = () => {
<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 /><hr />
<TextField name="composerUrl" label="Composer URL" placeholder="URL for the composer web page" />
<br />
<TextField name="rendererUrl" label="Renderer URL" placeholder="URL for the renderer template" />
<br /><br />
<CheckboxField name="agree">
I agree to the Apps <Link to="/terms/apps">terms and conditions</Link>.
</CheckboxField>
<br /><br />
<button className="button is-success">
<button className="button is-success" onClick={() => handleCreate()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>

6
src/components/pages/developers.tsx

@ -26,6 +26,12 @@ const Developers: FC = () => {
<div className="main-content">
<div className="centered-content">
<h1 className="title has-text-success">Developer Documentation</h1>
<p>Flexor apps allow users to express themselves on the network.</p>
<br />
<p>Developer documentation coming soon.</p>
<hr />
<p>This is where you manage apps you create.</p>
<br />

91
src/components/pages/view-app.tsx

@ -0,0 +1,91 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useParams, useHistory } from 'react-router-dom'
import moment from 'moment'
import { handleApiError } from 'src/api/errors'
import { fetchApp } from 'src/actions/apps'
import { getAuthenticatedUserId } from 'src/selectors/authentication'
import { getEntity } from 'src/selectors/entities'
import { setTitle } from 'src/utils'
import { AppState, AppThunkDispatch, EntityType, App } from 'src/types'
import PageHeader from 'src/components/page-header'
import Loading from 'src/components/pages/loading'
interface Params {
id: string
}
const ViewApp: FC = () => {
const { id } = useParams<Params>()
const app = useSelector<AppState, App | undefined>(state => getEntity<App>(state, EntityType.App, id))
const selfId = useSelector<AppState, string | undefined>(getAuthenticatedUserId)
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
useEffect(() => {
try {
dispatch(fetchApp(id))
} catch (err) {
handleApiError(err, dispatch, history)
}
}, [])
useEffect(() => {
if (app) setTitle(app.name)
}, [app])
if (!app) return <Loading />
const isCreator = app.user.id === selfId
return (
<div>
<PageHeader title={app.name} />
<div className="main-content">
<nav className="level">
<div className="level-item has-text-centered">
<div>
<p className="heading">Users</p>
<p className="title">0</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Rating</p>
<p className="title">{app.rating || '0'}</p>
</div>
</div>
{app.companyName &&
<div className="level-item has-text-centered">
<div>
<p className="heading">Company</p>
<p className="title">{app.companyName}</p>
</div>
</div>
}
<div className="level-item has-text-centered">
<div>
<p className="heading">Updated</p>
<p className="title">{moment(app.updated).format('MMMM Do, YYYY')}</p>
</div>
</div>
</nav>
<div className="centered-content">
<p>{app.about}</p>
</div>
</div>
</div>
)
}
export default ViewApp

6
src/selectors/entities.ts

@ -8,11 +8,7 @@ export const getEntity = <T extends Entity = Entity>(state: AppState, type: Enti
const entities = getEntityStore(state)
switch (type) {
case EntityType.User:
return denormalize([id], EntityType.User, entities)[0] as T
case EntityType.Group:
return denormalize([id], EntityType.Group, entities)[0] as T
default:
return
return denormalize([id], type, entities)[0] as T
}
}

2
src/styles/app.scss

@ -88,7 +88,7 @@ footer {
div#notification-container {
bottom: 10px;
position: absolute;
position: fixed;
left: 10px;
width: 40%;
}

3
src/types/entities.ts

@ -67,7 +67,8 @@ export type App = Entity & {
initCallbackUrl?: string
composeCallbackUrl?: string
rating: number
publicKey: string
publicKey?: string
privateKey?: string
active: boolean
updated: number
created: number

5
src/types/index.ts

@ -20,6 +20,11 @@ export interface Tab {
label: string
}
export interface AvailabilityResponse {
id: string
available: boolean
}
export * from './entities'
export * from './store'

21
src/types/store.ts

@ -9,20 +9,23 @@ export enum NotificationType {
}
export enum RequestKey {
Authenticate = 'authenticate',
CreateApp = 'create_app',
CreateGroup = 'create_group',
CreateInvitation = 'create_invitation',
FetchAppAvailability = 'fetch_app_availability',
FetchApp = 'fetch_app',
FetchApps = 'fetch_apps',
FetchGroup = 'fetch_group',
FetchGroups = 'fetch_groups',
FetchGroupAvailability = 'fetch_group_availability',
FetchUserAvailability = 'fetch_user_availability',
CreateGroup = 'create_group',
UpdateGroup = 'update_group',
Register = 'register',
Authenticate = 'authenticate',
FetchGroupMembers = 'fetch_group_members',
FetchGroupLogs = 'fetch_group_logs',
CreateInvitation = 'create_invitation',
FetchGroupMembers = 'fetch_group_members',
FetchGroups = 'fetch_groups',
FetchInvitations = 'fetch_invitations',
FetchApps = 'fetch_apps',
FetchSelfApps = 'fetch_self_apps',
FetchUserAvailability = 'fetch_user_availability',
Register = 'register',
UpdateGroup = 'update_group',
UpdateSelf = 'update_self',
}

Loading…
Cancel
Save