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 { Link } from 'react-router-dom' |
|||
|
|||
import { useConfig } from 'src/hooks' |
|||
import { App } from 'src/types' |
|||
|
|||
interface Props { |
|||
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 |
@ -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 { 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 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