Dwayne Harris
5 years ago
25 changed files with 531 additions and 177 deletions
-
7config/config.json
-
4src/actions/groups.ts
-
25src/actions/users.ts
-
44src/components/app-info.tsx
-
12src/components/app.tsx
-
15src/components/create-group-form.tsx
-
7src/components/create-user-form.tsx
-
16src/components/forms/cover-image-field.tsx
-
18src/components/forms/file-field.tsx
-
16src/components/forms/icon-image-field.tsx
-
16src/components/forms/image-field.tsx
-
10src/components/pages/create-app.tsx
-
10src/components/pages/edit-app.tsx
-
33src/components/pages/group-admin.tsx
-
29src/components/pages/self.tsx
-
61src/components/pages/view-app.tsx
-
50src/components/pages/view-group.tsx
-
108src/components/pages/view-user.tsx
-
79src/components/self-info.tsx
-
83src/components/user-info.tsx
-
5src/reducers/config.ts
-
9src/types/config.ts
-
3src/types/entities.ts
-
1src/types/index.ts
-
7src/types/store.ts
@ -1,4 +1,9 @@ |
|||
{ |
|||
"apiUrl": "http://localhost:5000", |
|||
"blobUrl": "https://flexordev.blob.core.windows.net/media/" |
|||
"blobUrl": "https://flexordev.blob.core.windows.net/media/", |
|||
"media": { |
|||
"defaultMaxSize": 5242880, |
|||
"coverMaxSize": 5242880, |
|||
"iconMaxSize": 1048576 |
|||
} |
|||
} |
@ -0,0 +1,25 @@ |
|||
import { apiFetch } from 'src/api' |
|||
import { setEntities } from 'src/actions/entities' |
|||
import { startRequest, finishRequest } from 'src/actions/requests' |
|||
import { AppThunkAction, Entity, RequestKey, EntityType } from 'src/types' |
|||
import { normalize } from 'src/utils/normalization' |
|||
|
|||
export const fetchUser = (id: string): AppThunkAction => { |
|||
return async dispatch => { |
|||
dispatch(startRequest(RequestKey.FetchUser)) |
|||
|
|||
try { |
|||
const user = await apiFetch<Entity>({ |
|||
path: `/api/user/${id}` |
|||
}) |
|||
|
|||
const users = normalize([user], EntityType.User) |
|||
|
|||
dispatch(setEntities(users.entities)) |
|||
dispatch(finishRequest(RequestKey.FetchUser, true)) |
|||
} catch (err) { |
|||
dispatch(finishRequest(RequestKey.FetchUser, false)) |
|||
throw err |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
import React, { FC } from 'react' |
|||
import moment from 'moment' |
|||
|
|||
import { App } from 'src/types' |
|||
|
|||
interface Props { |
|||
app: App |
|||
} |
|||
|
|||
const AppInfo: FC<Props> = ({ app }) => ( |
|||
<nav className="level"> |
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Users</p> |
|||
<p className="title">{app.users}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Rating</p> |
|||
<p className="title">{app.rating}</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> |
|||
) |
|||
|
|||
export default AppInfo |
@ -0,0 +1,16 @@ |
|||
import React, { FC } from 'react' |
|||
import { useConfig } from 'src/hooks' |
|||
import FileField from './file-field' |
|||
|
|||
interface Props { |
|||
name: string |
|||
label?: string |
|||
help?: string |
|||
} |
|||
|
|||
const CoverImageField: FC<Props> = ({ name, label = 'Cover Image', help = 'Approx 400 x 200. Max 5 MBs.' }) => { |
|||
const config = useConfig() |
|||
return <FileField name={name} label={label} help={help} previewWidth={200} maxSize={config.media.coverMaxSize} /> |
|||
} |
|||
|
|||
export default CoverImageField |
@ -0,0 +1,16 @@ |
|||
import React, { FC } from 'react' |
|||
import { useConfig } from 'src/hooks' |
|||
import FileField from './file-field' |
|||
|
|||
interface Props { |
|||
name: string |
|||
label?: string |
|||
help?: string |
|||
} |
|||
|
|||
const IconImageField: FC<Props> = ({ name, label = 'Icon Image', help = 'Approx 32 x 32. Max 1 MB.' }) => { |
|||
const config = useConfig() |
|||
return <FileField name={name} label={label} help={help} previewWidth={32} maxSize={config.media.iconMaxSize} /> |
|||
} |
|||
|
|||
export default IconImageField |
@ -0,0 +1,16 @@ |
|||
import React, { FC } from 'react' |
|||
import { useConfig } from 'src/hooks' |
|||
import FileField from './file-field' |
|||
|
|||
interface Props { |
|||
name: string |
|||
label?: string |
|||
help?: string |
|||
} |
|||
|
|||
const ImageField: FC<Props> = ({ name, label = 'Image', help = 'Approx 128 x 128. Max 5 MBs.' }) => { |
|||
const config = useConfig() |
|||
return <FileField name={name} label={label} help={help} previewWidth={64} maxSize={config.media.defaultMaxSize} /> |
|||
} |
|||
|
|||
export default ImageField |
@ -0,0 +1,108 @@ |
|||
import React, { FC, useEffect } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { Link, useParams, useHistory } from 'react-router-dom' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faUserPlus, faUserMinus, faBan } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import { handleApiError } from 'src/api/errors' |
|||
import { fetchUser } from 'src/actions/users' |
|||
import { getEntity } from 'src/selectors/entities' |
|||
|
|||
import { useDeepCompareEffect, useConfig } from 'src/hooks' |
|||
import { setTitle, urlForBlob } from 'src/utils' |
|||
import { AppState, EntityType, User, AppThunkDispatch } from 'src/types' |
|||
|
|||
import PageHeader from 'src/components/page-header' |
|||
import UserInfo from 'src/components/user-info' |
|||
import Loading from 'src/components/pages/loading' |
|||
import GroupAdmin from './group-admin' |
|||
|
|||
interface Params { |
|||
id: string |
|||
} |
|||
|
|||
const ViewUser: FC = () => { |
|||
const { id } = useParams<Params>() |
|||
const user = useSelector<AppState, User | undefined>(state => getEntity<User>(state, EntityType.User, id)) |
|||
const dispatch = useDispatch<AppThunkDispatch>() |
|||
const config = useConfig() |
|||
const history = useHistory() |
|||
|
|||
useEffect(() => { |
|||
try { |
|||
dispatch(fetchUser(id)) |
|||
} catch (err) { |
|||
handleApiError(err, dispatch, history) |
|||
} |
|||
}, []) |
|||
|
|||
useDeepCompareEffect(() => { |
|||
if (user) setTitle(user.name) |
|||
}, [user]) |
|||
|
|||
if (!user) return <Loading /> |
|||
const imageUrl = user.imageUrl ? urlForBlob(config, user.imageUrl) : undefined |
|||
|
|||
return ( |
|||
<div> |
|||
<PageHeader title={user.name} /> |
|||
|
|||
<div className="main-content"> |
|||
<UserInfo user={user} /> |
|||
|
|||
<div className="centered-content"> |
|||
<article className="media"> |
|||
{imageUrl && |
|||
<figure className="media-left"> |
|||
<p className="image is-32x32"> |
|||
<img src={imageUrl} style={{ width: 128 }} /> |
|||
</p> |
|||
</figure> |
|||
} |
|||
<div className="media-content"> |
|||
<div className="content"> |
|||
<h1 className="is-size-3"> |
|||
<span className="is-size-3">{user.name}</span> <span className="is-size-4 has-text-weight-bold">@{user.id}</span> |
|||
</h1> |
|||
{user.group && <h2 className="is-size-4">Community: <Link to={`/c/${user.group.id}`}>{user.group.name}</Link></h2>} |
|||
<br /> |
|||
|
|||
<p>{user.about}</p> |
|||
</div> |
|||
</div> |
|||
</article> |
|||
<br /> |
|||
|
|||
<div className="buttons"> |
|||
<button className="button is-success"> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faUserPlus} /> |
|||
</span> |
|||
<span>Susbcribe</span> |
|||
</button> |
|||
|
|||
<button className="button is-danger"> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faBan} /> |
|||
</span> |
|||
<span>Block</span> |
|||
</button> |
|||
|
|||
{user.group && |
|||
<button className="button is-danger"> |
|||
<span className="icon is-small"> |
|||
<FontAwesomeIcon icon={faBan} /> |
|||
</span> |
|||
<span>Block Community: {user.group.name}</span> |
|||
</button> |
|||
} |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default ViewUser |
@ -0,0 +1,79 @@ |
|||
import React, { FC } from 'react' |
|||
import { useSelector } from 'react-redux' |
|||
import { Link } from 'react-router-dom' |
|||
|
|||
import { getConfig } from 'src/selectors' |
|||
import { getAuthenticatedUser } from 'src/selectors/authentication' |
|||
import { urlForBlob } from 'src/utils' |
|||
import { AppState, User, Config } from 'src/types' |
|||
|
|||
const SelfInfo: FC = () => { |
|||
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser) |
|||
const config = useSelector<AppState, Config>(getConfig) |
|||
|
|||
const imageUrl = user && user.imageUrl ? urlForBlob(config, user.imageUrl) : undefined |
|||
|
|||
const name = (user: User) => { |
|||
if (user.name) { |
|||
return ( |
|||
<Link to="/self" className="has-text-white"> |
|||
<span className="is-size-5">{user.name}</span> <span className="is-size-6 has-text-weight-bold">@{user.id}</span> |
|||
</Link> |
|||
) |
|||
} |
|||
|
|||
return <Link to="/self" className="is-size-4 has-text-white-ter">@{user.id}</Link> |
|||
} |
|||
|
|||
const content = () => { |
|||
if (user) { |
|||
const group = user.group |
|||
const groupImageUrl = group && group.iconImageUrl ? urlForBlob(config, group.iconImageUrl) : undefined |
|||
|
|||
return ( |
|||
<div> |
|||
{name(user)} |
|||
<br /> |
|||
{group && |
|||
<div> |
|||
{groupImageUrl && |
|||
<figure className="image is-16x16 is-inline"> |
|||
<img src={groupImageUrl} style={{ width: 16 }} /> |
|||
</figure> |
|||
} |
|||
|
|||
<Link to={`/c/${group.id}`} className="is-size-5 has-text-success">{group.name}</Link> |
|||
</div> |
|||
} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<div className="has-text-centered"> |
|||
<Link to="/login" className="is-size-5 has-text-white">Log In to Flexor</Link> |
|||
<p className="is-size-7 has-text-primary is-uppercase">or</p> |
|||
<Link to="/communities" className="is-size-6 has-text-light">Create an Account</Link> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<article id="user-info" className="media has-background-black"> |
|||
{imageUrl && |
|||
<figure className="media-left"> |
|||
<p className="image is-32x32"> |
|||
<img src={imageUrl} style={{ width: 32 }} /> |
|||
</p> |
|||
</figure> |
|||
} |
|||
<div className="media-content"> |
|||
<div className="content"> |
|||
{content()} |
|||
</div> |
|||
</div> |
|||
</article> |
|||
) |
|||
} |
|||
|
|||
export default SelfInfo |
@ -1,71 +1,42 @@ |
|||
import React, { FC } from 'react' |
|||
import { useSelector } from 'react-redux' |
|||
import { Link } from 'react-router-dom' |
|||
import moment from 'moment' |
|||
|
|||
import { getConfig } from 'src/selectors' |
|||
import { getAuthenticatedUser } from 'src/selectors/authentication' |
|||
import { urlForBlob } from 'src/utils' |
|||
import { AppState, User, Config } from 'src/types' |
|||
import { User } from 'src/types' |
|||
|
|||
const UserInfo: FC = () => { |
|||
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser) |
|||
const config = useSelector<AppState, Config>(getConfig) |
|||
|
|||
const imageUrl = user && user.imageUrl ? urlForBlob(config, user.imageUrl) : undefined |
|||
|
|||
const name = (user: User) => { |
|||
if (user.name) return <Link to="/self" className="is-size-6 has-text-white">{user.name} @{user.id}</Link> |
|||
return <Link to="/self" className="is-size-4 has-text-white-ter">@{user.id}</Link> |
|||
} |
|||
|
|||
const content = () => { |
|||
if (user) { |
|||
const group = user.group |
|||
const groupImageUrl = group && group.coverImageUrl ? urlForBlob(config, group.coverImageUrl) : undefined |
|||
interface Props { |
|||
user: User |
|||
} |
|||
|
|||
return ( |
|||
const UserInfo: FC<Props> = ({ user }) => ( |
|||
<nav className="level"> |
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
{name(user)} |
|||
<br /> |
|||
{group && |
|||
<p className="heading">Posts</p> |
|||
<p className="title">{user.posts}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
{groupImageUrl && |
|||
<figure className="image is-16x16"> |
|||
<img src="groupImageUrl" style={{ width: 16 }} /> |
|||
</figure> |
|||
} |
|||
<Link to={`/c/${group.id}`} className="is-size-5 has-text-success">{group.name}</Link> |
|||
<p className="heading has-text-success">Awards</p> |
|||
<p className="title">{user.awards}</p> |
|||
</div> |
|||
} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<div className="has-text-centered"> |
|||
<Link to="/login" className="is-size-5 has-text-white">Log In to Flexor</Link> |
|||
<p className="is-size-7 has-text-primary is-uppercase">or</p> |
|||
<Link to="/communities" className="is-size-6 has-text-light">Create an Account</Link> |
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Points</p> |
|||
<p className="title">{user.points}</p> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<article id="user-info" className="media has-background-black"> |
|||
{imageUrl && |
|||
<figure className="media-left"> |
|||
<p className="image is-32x32"> |
|||
<img src={imageUrl} style={{ width: 32 }} /> |
|||
</p> |
|||
</figure> |
|||
} |
|||
<div className="media-content"> |
|||
<div className="content"> |
|||
{content()} |
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Joined</p> |
|||
<p className="title is-size-5">{moment(user.created).format('MMMM Do, YYYY')}</p> |
|||
</div> |
|||
</div> |
|||
</article> |
|||
) |
|||
} |
|||
</nav> |
|||
) |
|||
|
|||
export default UserInfo |
@ -0,0 +1,9 @@ |
|||
export interface Config { |
|||
apiUrl: string |
|||
blobUrl: string |
|||
media: { |
|||
defaultMaxSize: number |
|||
coverMaxSize: number |
|||
iconMaxSize: number |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue