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
-
53src/components/pages/self.tsx
-
67src/components/pages/view-app.tsx
-
52src/components/pages/view-group.tsx
-
108src/components/pages/view-user.tsx
-
79src/components/self-info.tsx
-
91src/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", |
"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 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> |
|
||||
} |
|
||||
|
interface Props { |
||||
|
user: User |
||||
|
} |
||||
|
|
||||
const content = () => { |
|
||||
if (user) { |
|
||||
const group = user.group |
|
||||
const groupImageUrl = group && group.coverImageUrl ? urlForBlob(config, group.coverImageUrl) : undefined |
|
||||
|
const UserInfo: FC<Props> = ({ user }) => ( |
||||
|
<nav className="level"> |
||||
|
<div className="level-item has-text-centered"> |
||||
|
<div> |
||||
|
<p className="heading">Posts</p> |
||||
|
<p className="title">{user.posts}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
return ( |
|
||||
<div> |
|
||||
{name(user)} |
|
||||
<br /> |
|
||||
{group && |
|
||||
<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> |
|
||||
</div> |
|
||||
} |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
|
<div className="level-item has-text-centered"> |
||||
|
<div> |
||||
|
<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> |
||||
) |
|
||||
} |
|
||||
|
</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 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> |
|
||||
) |
|
||||
} |
|
||||
|
</div> |
||||
|
</nav> |
||||
|
) |
||||
|
|
||||
export default UserInfo |
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