Dwayne Harris
5 years ago
28 changed files with 548 additions and 162 deletions
-
41src/actions/apps.ts
-
52src/actions/composer.ts
-
14src/actions/registration.ts
-
29src/components/app-list-item.tsx
-
46src/components/composer.tsx
-
32src/components/create-group-form.tsx
-
2src/components/create-group-step.tsx
-
18src/components/create-user-form.tsx
-
4src/components/create-user-step.tsx
-
76src/components/forms/file-field.tsx
-
9src/components/forms/text-field.tsx
-
58src/components/pages/edit-app.tsx
-
10src/components/pages/home.tsx
-
4src/components/pages/register-group.tsx
-
10src/components/pages/register.tsx
-
2src/components/pages/self.tsx
-
16src/components/pages/view-app.tsx
-
63src/components/user-info.tsx
-
5src/hooks/index.ts
-
28src/reducers/composer.ts
-
2src/selectors/apps.ts
-
11src/selectors/composer.ts
-
2src/store/index.ts
-
36src/styles/app.scss
-
45src/types/entities.ts
-
7src/types/store.ts
-
14src/utils/index.ts
-
74src/utils/normalization.ts
@ -0,0 +1,52 @@ |
|||||
|
import { Action } from 'redux' |
||||
|
|
||||
|
import { setEntities } from 'src/actions/entities' |
||||
|
import { startRequest, finishRequest } from 'src/actions/requests' |
||||
|
import { apiFetch } from 'src/api' |
||||
|
import { normalize } from 'src/utils/normalization' |
||||
|
import { AppThunkAction, Installation, RequestKey, EntityType } from 'src/types' |
||||
|
|
||||
|
export interface SetInstallationsAction extends Action { |
||||
|
type: 'COMPOSER_SET_INSTALLATIONS' |
||||
|
payload: string[] |
||||
|
} |
||||
|
|
||||
|
export interface SetSelectedInstallationAction extends Action { |
||||
|
type: 'COMPOSER_SET_SELECTED_INSTALLATION' |
||||
|
payload?: string |
||||
|
} |
||||
|
|
||||
|
export type ComposerActions = SetInstallationsAction | SetSelectedInstallationAction |
||||
|
|
||||
|
export const setInstallations = (installations: string[]): SetInstallationsAction => ({ |
||||
|
type: 'COMPOSER_SET_INSTALLATIONS', |
||||
|
payload: installations, |
||||
|
}) |
||||
|
|
||||
|
export const setSelectedInstallation = (installation?: string): SetSelectedInstallationAction => ({ |
||||
|
type: 'COMPOSER_SET_SELECTED_INSTALLATION', |
||||
|
payload: installation, |
||||
|
}) |
||||
|
|
||||
|
interface FetchInstallationsResponse { |
||||
|
installations: Installation[] |
||||
|
} |
||||
|
|
||||
|
export const fetchInstallations = (): AppThunkAction => async dispatch => { |
||||
|
dispatch(startRequest(RequestKey.FetchInstallations)) |
||||
|
|
||||
|
try { |
||||
|
const response = await apiFetch<FetchInstallationsResponse>({ |
||||
|
path: '/api/installations', |
||||
|
}) |
||||
|
|
||||
|
const result = normalize(response.installations, EntityType.Installation) |
||||
|
|
||||
|
dispatch(setEntities(result.entities)) |
||||
|
dispatch(setInstallations(result.keys)) |
||||
|
dispatch(finishRequest(RequestKey.FetchInstallations, true)) |
||||
|
} catch (err) { |
||||
|
dispatch(finishRequest(RequestKey.FetchInstallations, false)) |
||||
|
throw err |
||||
|
} |
||||
|
} |
@ -1,16 +1,33 @@ |
|||||
import React, { FC } from 'react' |
import React, { FC } from 'react' |
||||
import { Link } from 'react-router-dom' |
import { Link } from 'react-router-dom' |
||||
|
|
||||
|
import { useConfig } from 'src/hooks' |
||||
import { App } from 'src/types' |
import { App } from 'src/types' |
||||
|
|
||||
interface Props { |
interface Props { |
||||
app: App |
app: App |
||||
} |
} |
||||
|
|
||||
const AppListItem: FC<Props> = ({ app }) => ( |
|
||||
<div className="app-list-item"> |
|
||||
<Link to={`/a/${app.id}`} className="title has-text-primary">{app.name}</Link> |
|
||||
{app.about && <p>{app.about}</p>} |
|
||||
</div> |
|
||||
) |
|
||||
|
const AppListItem: FC<Props> = ({ app }) => { |
||||
|
const config = useConfig() |
||||
|
|
||||
|
return ( |
||||
|
<article className="media"> |
||||
|
{app.imageUrl && |
||||
|
<figure className="media-left"> |
||||
|
<p className="image is-64x64"> |
||||
|
<img src={`${config.blobUrl}${app.imageUrl}`} style={{ width: 64 }} /> |
||||
|
</p> |
||||
|
</figure> |
||||
|
} |
||||
|
<div className="media-content"> |
||||
|
<div className="content"> |
||||
|
<Link to={`/a/${app.id}`} className="title has-text-primary">{app.name}</Link> |
||||
|
{app.about && <p>{app.about}</p>} |
||||
|
</div> |
||||
|
</div> |
||||
|
</article> |
||||
|
) |
||||
|
} |
||||
|
|
||||
export default AppListItem |
export default AppListItem |
@ -0,0 +1,46 @@ |
|||||
|
import React, { FC, useEffect } from 'react' |
||||
|
import { useSelector, useDispatch } from 'react-redux' |
||||
|
|
||||
|
import { useConfig } from 'src/hooks' |
||||
|
import { fetchInstallations, setSelectedInstallation } from 'src/actions/composer' |
||||
|
import { getInstallations, getSelectedInstallation } from 'src/selectors/composer' |
||||
|
import { AppState, Installation } from 'src/types' |
||||
|
|
||||
|
const Composer: FC = () => { |
||||
|
const installations = useSelector<AppState, Installation[]>(getInstallations) |
||||
|
const selected = useSelector<AppState, Installation | undefined>(getSelectedInstallation) |
||||
|
const config = useConfig() |
||||
|
const dispatch = useDispatch() |
||||
|
|
||||
|
useEffect(() => { |
||||
|
dispatch(fetchInstallations()) |
||||
|
}, []) |
||||
|
|
||||
|
const handleClick = (id: string) => { |
||||
|
if (selected && selected.id === id) { |
||||
|
dispatch(setSelectedInstallation()) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
dispatch(setSelectedInstallation(id)) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className="container composer-container"> |
||||
|
<div className="composer composer-empty"> |
||||
|
<p>{selected ? selected.app.name : 'Choose an app.'}</p> |
||||
|
</div> |
||||
|
|
||||
|
<div className="installations is-flex"> |
||||
|
{installations.map(installation => ( |
||||
|
<div key={installation.id} className={selected && selected.id === installation.id ? 'selected' : ''} onClick={() => handleClick(installation.id)}> |
||||
|
<img src={`${config.blobUrl}${installation.app.iconImageUrl}`} alt={installation.app.name} style={{ width: 32 }} /> |
||||
|
<p className="is-size-7 has-text-weight-bold">{installation.app.name}</p> |
||||
|
</div> |
||||
|
))} |
||||
|
</div> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Composer |
@ -0,0 +1,28 @@ |
|||||
|
import { Reducer } from 'redux' |
||||
|
|
||||
|
import { ComposerActions } from '../actions/composer' |
||||
|
import { ComposerState } from '../types' |
||||
|
|
||||
|
const initialState: ComposerState = { |
||||
|
installations: [], |
||||
|
selected: undefined, |
||||
|
} |
||||
|
|
||||
|
const reducer: Reducer<ComposerState, ComposerActions> = (state = initialState, action) => { |
||||
|
switch (action.type) { |
||||
|
case 'COMPOSER_SET_INSTALLATIONS': |
||||
|
return { |
||||
|
...state, |
||||
|
installations: action.payload, |
||||
|
} |
||||
|
case 'COMPOSER_SET_SELECTED_INSTALLATION': |
||||
|
return { |
||||
|
...state, |
||||
|
selected: action.payload, |
||||
|
} |
||||
|
default: |
||||
|
return state |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default reducer |
@ -1,5 +1,5 @@ |
|||||
import { denormalize } from 'src/utils/normalization' |
import { denormalize } from 'src/utils/normalization' |
||||
import { AppState, EntityType, App } from 'src/types' |
|
||||
|
import { AppState, EntityType, App, Installation } from 'src/types' |
||||
|
|
||||
export const getApps = (state: AppState) => denormalize(state.apps.items, EntityType.App, state.entities) as App[] |
export const getApps = (state: AppState) => denormalize(state.apps.items, EntityType.App, state.entities) as App[] |
||||
export const getCreatedApps = (state: AppState) => denormalize(state.apps.created.items, EntityType.App, state.entities) as App[] |
export const getCreatedApps = (state: AppState) => denormalize(state.apps.created.items, EntityType.App, state.entities) as App[] |
@ -0,0 +1,11 @@ |
|||||
|
import { denormalize } from 'src/utils/normalization' |
||||
|
import { AppState, EntityType, App, Installation } from 'src/types' |
||||
|
|
||||
|
export const getApps = (state: AppState) => denormalize(state.apps.items, EntityType.App, state.entities) as App[] |
||||
|
export const getCreatedApps = (state: AppState) => denormalize(state.apps.created.items, EntityType.App, state.entities) as App[] |
||||
|
export const getInstallations = (state: AppState) => denormalize(state.composer.installations, EntityType.Installation, state.entities) as Installation[] |
||||
|
|
||||
|
export const getSelectedInstallation = (state: AppState) => { |
||||
|
if (!state.composer.selected) return |
||||
|
return denormalize([state.composer.selected], EntityType.Installation, state.entities)[0] as Installation |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue