Dwayne Harris 5 years ago
parent
commit
b0449ccfb7
  1. 7
      config/config.json
  2. 4
      src/actions/groups.ts
  3. 25
      src/actions/users.ts
  4. 44
      src/components/app-info.tsx
  5. 12
      src/components/app.tsx
  6. 15
      src/components/create-group-form.tsx
  7. 7
      src/components/create-user-form.tsx
  8. 16
      src/components/forms/cover-image-field.tsx
  9. 18
      src/components/forms/file-field.tsx
  10. 16
      src/components/forms/icon-image-field.tsx
  11. 16
      src/components/forms/image-field.tsx
  12. 10
      src/components/pages/create-app.tsx
  13. 10
      src/components/pages/edit-app.tsx
  14. 33
      src/components/pages/group-admin.tsx
  15. 53
      src/components/pages/self.tsx
  16. 67
      src/components/pages/view-app.tsx
  17. 52
      src/components/pages/view-group.tsx
  18. 108
      src/components/pages/view-user.tsx
  19. 79
      src/components/self-info.tsx
  20. 91
      src/components/user-info.tsx
  21. 5
      src/reducers/config.ts
  22. 9
      src/types/config.ts
  23. 3
      src/types/entities.ts
  24. 1
      src/types/index.ts
  25. 7
      src/types/store.ts

7
config/config.json

@ -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
}
}

4
src/actions/groups.ts

@ -91,7 +91,9 @@ export const fetchGroup = (id: string): AppThunkAction => {
path: `/api/group/${id}`
})
dispatch(setEntity(EntityType.Group, group))
const groups = normalize([group], EntityType.Group)
dispatch(setEntities(groups.entities))
dispatch(finishRequest(RequestKey.FetchGroup, true))
} catch (err) {
dispatch(finishRequest(RequestKey.FetchGroup, false))

25
src/actions/users.ts

@ -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
}
}
}

44
src/components/app-info.tsx

@ -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

12
src/components/app.tsx

@ -16,14 +16,13 @@ import Footer from './footer'
import NavigationMenu from './navigation-menu'
import NotificationContainer from './notification-container'
import Spinner from './spinner'
import UserInfo from './user-info'
import SelfInfo from './self-info'
import About from './pages/about'
import Apps from './pages/apps'
import CreateApp from './pages/create-app'
import Developers from './pages/developers'
import EditApp from './pages/edit-app'
import Group from './pages/group'
import GroupAdmin from './pages/group-admin'
import Groups from './pages/groups'
import Home from './pages/home'
@ -32,6 +31,8 @@ import Register from './pages/register'
import RegisterGroup from './pages/register-group'
import Self from './pages/self'
import ViewApp from './pages/view-app'
import ViewGroup from './pages/view-group'
import ViewUser from './pages/view-user'
import '../styles/app.scss'
import '../styles/spinner.scss'
@ -76,7 +77,7 @@ const App: FC = () => {
<NavigationMenu />
{fetching && <Spinner />}
<UserInfo />
<SelfInfo />
<Footer />
</div>
@ -89,7 +90,7 @@ const App: FC = () => {
<RegisterGroup />
</Route>
<Route path="/c/:id">
<Group />
<ViewGroup />
</Route>
<Route path="/a/:id/edit">
<EditApp />
@ -97,6 +98,9 @@ const App: FC = () => {
<Route path="/a/:id">
<ViewApp />
</Route>
<Route path="/u/:id">
<ViewUser />
</Route>
<Route path="/login">
<Login />
</Route>

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

@ -1,15 +1,16 @@
import React, { FC, FocusEventHandler } from 'react'
import React, { FC } from 'react'
import { useDispatch } from 'react-redux'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUpload, faIdCard } from '@fortawesome/free-solid-svg-icons'
import { faIdCard } from '@fortawesome/free-solid-svg-icons'
import { checkGroupAvailability } from 'src/actions/registration'
import CheckboxField from 'src/components/forms/checkbox-field'
import TextField from 'src/components/forms/text-field'
import SelectField from 'src/components/forms/select-field'
import FileField from 'src/components/forms/file-field'
import ImageField from 'src/components/forms/image-field'
import CoverImageField from 'src/components/forms/cover-image-field'
import IconImageField from 'src/components/forms/icon-image-field'
const CreateGroupForm: FC = () => {
const dispatch = useDispatch()
@ -32,11 +33,11 @@ const CreateGroupForm: FC = () => {
<br />
<SelectField name="group-registration" label="Registration" options={registrationOptions} icon={faIdCard} />
<br />
<FileField name="group-image" label="Community Image" help="Approx 128 x 128. Max 5 MBs." />
<ImageField name="group-image" label="Community Image" />
<br />
<FileField name="group-cover-image" label="Cover Image" help="Approx 400 x 200. Max 5 MBs." />
<CoverImageField name="group-cover-image" />
<br />
<FileField name="group-icon-image" label="Icon Image" help="Approx 32 x 32. Max 5 MBs." />
<IconImageField name="group-icon-image" />
<br />
<CheckboxField name="group-agree">
I agree to the Communities <Link to="/terms/communities">terms and conditions</Link>.

7
src/components/create-user-form.tsx

@ -9,7 +9,8 @@ import CheckboxField from 'src/components/forms/checkbox-field'
import TextField from 'src/components/forms/text-field'
import PasswordField from 'src/components/forms/password-field'
import SelectField from 'src/components/forms/select-field'
import FileField from 'src/components/forms/file-field'
import ImageField from 'src/components/forms/image-field'
import CoverImageField from 'src/components/forms/cover-image-field'
const CreateUserForm: FC = () => {
const dispatch = useDispatch()
@ -30,9 +31,9 @@ const CreateUserForm: FC = () => {
<br />
<PasswordField placeholder="Your new password" />
<br />
<FileField name="user-image" label="Avatar" help="Approx 128 x 128. Max 5 MBs." previewWidth={64} />
<ImageField name="user-image" label="Avatar" />
<br />
<FileField name="user-cover-image" label="Cover Image" help="Approx 400 x 200. Max 5 MBs." />
<CoverImageField name="user-cover-image" />
<br />
<SelectField name="user-privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<br />

16
src/components/forms/cover-image-field.tsx

@ -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

18
src/components/forms/file-field.tsx

@ -5,25 +5,28 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUpload } from '@fortawesome/free-solid-svg-icons'
import { uploadBrowserDataToBlockBlob, Aborter, BlockBlobURL, AnonymousCredential } from '@azure/storage-blob'
import { useConfig } from 'src/hooks'
import { setFieldValue } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { getConfig } from 'src/selectors'
import { getFieldValue } from 'src/selectors/forms'
import { apiFetch } from 'src/api/fetch'
import { AppState, ClassDictionary, SasResponse, NotificationType, Config } from 'src/types'
import { AppState, ClassDictionary, SasResponse, NotificationType } from 'src/types'
interface Props {
name: string
label: string
help?: string
previewWidth?: number
maxSize?: number
}
const FileField: FC<Props> = ({ name, label, help, previewWidth = 128 }) => {
const value = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, name, false))
const config = useSelector<AppState, Config>(getConfig)
const FileField: FC<Props> = props => {
const value = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, props.name, false))
const config = useConfig()
const dispatch = useDispatch()
const { name, label, help, previewWidth = 128, maxSize = config.media.defaultMaxSize } = props
const [progress, setProgress] = useState(0)
const [uploading, setUploading] = useState(false)
const [uploaded, setUploaded] = useState(false)
@ -38,8 +41,9 @@ const FileField: FC<Props> = ({ name, label, help, previewWidth = 128 }) => {
if (event.target.files) {
const file = event.target.files[0]
if (file.size > 1024 * 1024 * 5) {
dispatch(showNotification(NotificationType.Error, 'Files must be less than 5 MBs'))
if (file.size > maxSize) {
const maxSizeString = Math.round(maxSize / 1024 / 1024)
dispatch(showNotification(NotificationType.Error, `Files must be less than ${maxSizeString} MBs`))
return
}

16
src/components/forms/icon-image-field.tsx

@ -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

16
src/components/forms/image-field.tsx

@ -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

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

@ -15,6 +15,9 @@ 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'
import ImageField from 'src/components/forms/image-field'
import CoverImageField from 'src/components/forms/cover-image-field'
import IconImageField from 'src/components/forms/icon-image-field'
const CreateApp: FC = () => {
const form = useSelector<AppState, Form>(getForm)
@ -101,6 +104,13 @@ const CreateApp: FC = () => {
<TextField name="version" label="Version" placeholder="Current Version of the app (ex: 0.0.1beta5)" />
<br /><hr />
<ImageField name="image" />
<br />
<CoverImageField name="coverImage" />
<br />
<IconImageField name="iconImage" />
<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" />

10
src/components/pages/edit-app.tsx

@ -21,7 +21,9 @@ import Loading from 'src/components/pages/loading'
import TextField from 'src/components/forms/text-field'
import TextareaField from 'src/components/forms/textarea-field'
import FileField from 'src/components/forms/file-field'
import ImageField from 'src/components/forms/image-field'
import CoverImageField from 'src/components/forms/cover-image-field'
import IconImageField from 'src/components/forms/icon-image-field'
interface Params {
id: string
@ -141,11 +143,11 @@ const EditApp: FC = () => {
<TextField name="version" label="Version" placeholder="Current Version of the app (ex: 0.0.1beta5)" help={`Last Version: ${app.version}`} />
<br /><hr />
<FileField name="image" label="Image" />
<ImageField name="image" />
<br />
<FileField name="coverImage" label="Cover Image" />
<CoverImageField name="coverImage" />
<br />
<FileField name="iconImage" label="Icon Image" />
<IconImageField name="iconImage" />
<br /><hr />
<TextField name="composerUrl" label="Composer URL" placeholder="URL for the composer web page" />

33
src/components/pages/group-admin.tsx

@ -5,13 +5,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons'
import { handleApiError } from 'src/api/errors'
import { initForm, initField, setFieldValue } from 'src/actions/forms'
import { initForm, initField } from 'src/actions/forms'
import { fetchGroup, updateGroup } from 'src/actions/groups'
import { getEntity } from 'src/selectors/entities'
import { getFieldValue } from 'src/selectors/forms'
import { getForm } from 'src/selectors/forms'
import { useDeepCompareEffect } from 'src/hooks'
import { setTitle } from 'src/utils'
import { setTitle, valueFromForm } from 'src/utils'
import {
AppState,
AppThunkDispatch,
@ -19,15 +19,20 @@ import {
GroupMembershipType,
Tab,
EntityType,
Form,
} from 'src/types'
import PageHeader from 'src/components/page-header'
import TextareaField from 'src/components/forms/textarea-field'
import MemberList from 'src/components/member-list'
import GroupInvitations from 'src/components/group-invitations'
import GroupLogs from 'src/components/group-logs'
import Loading from 'src/components/pages/loading'
import TextareaField from 'src/components/forms/textarea-field'
import ImageField from 'src/components/forms/image-field'
import CoverImageField from 'src/components/forms/cover-image-field'
import IconImageField from 'src/components/forms/icon-image-field'
interface Params {
id: string
tab: string
@ -43,7 +48,7 @@ const GroupAdmin: FC = () => {
const { id, tab = '' } = useParams<Params>()
const history = useHistory()
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
const about = useSelector<AppState, string>(state => getFieldValue<string>(state, 'about', ''))
const form = useSelector<AppState, Form>(getForm)
const dispatch = useDispatch<AppThunkDispatch>()
@ -66,6 +71,9 @@ const GroupAdmin: FC = () => {
dispatch(initField('about', group.about))
dispatch(initField('expiration', '0'))
dispatch(initField('limit', '0'))
dispatch(initField('image', group.imageUrl))
dispatch(initField('coverImage', group.coverImageUrl))
dispatch(initField('iconImage', group.iconImageUrl))
setTitle(`${group.name} Administration`)
}
@ -73,7 +81,13 @@ const GroupAdmin: FC = () => {
const handleUpdateGroup = async () => {
try {
await dispatch(updateGroup(id, { about }))
await dispatch(updateGroup(id, {
about: valueFromForm<string>(form, 'about'),
imageUrl: valueFromForm<string>(form, 'image'),
coverImageUrl: valueFromForm<string>(form, 'coverImage'),
iconImageUrl: valueFromForm<string>(form, 'iconImage'),
}))
await dispatch(fetchGroup(id))
} catch (err) {
handleApiError(err, dispatch, history)
@ -122,6 +136,13 @@ const GroupAdmin: FC = () => {
<TextareaField name="about" label="About" placeholder="About this Community" />
<br />
<ImageField name="image" />
<br />
<CoverImageField name="coverImage" />
<br />
<IconImageField name="iconImage" />
<br /><br />
<button className="button is-primary" onClick={e => handleUpdateGroup()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />

53
src/components/pages/self.tsx

@ -18,11 +18,13 @@ import { AppState, User, Tab, Form } from 'src/types'
import PageHeader from 'src/components/page-header'
import Loading from 'src/components/pages/loading'
import UserInfo from 'src/components/user-info'
import TextField from 'src/components/forms/text-field'
import TextareaField from 'src/components/forms/textarea-field'
import SelectField from 'src/components/forms/select-field'
import CheckboxField from 'src/components/forms/checkbox-field'
import FileField from 'src/components/forms/file-field'
import ImageField from 'src/components/forms/image-field'
import CoverImageField from 'src/components/forms/cover-image-field'
interface Params {
tab: string
@ -93,14 +95,7 @@ const Self: FC = () => {
<PageHeader title={user.name || user.id} subtitle={`@${user.id}`} />
<div className="main-content">
<nav className="level">
<div className="level-item has-text-centered">
<div>
<div className="heading">Joined</div>
<div className="title">{moment(user.created).format('MMMM Do YYYY')}</div>
</div>
</div>
</nav>
<UserInfo user={user} />
<div className="centered-content">
<div className="tabs is-large">
@ -150,30 +145,38 @@ const Self: FC = () => {
<br />
<SelectField name="privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<br />
<FileField name="image" label="Image" />
<ImageField name="image" label="Avatar" />
<br />
<FileField name="coverImage" label="Cover Image" />
<CoverImageField name="coverImage" />
<br />
<CheckboxField name="requiresApproval">
You must approve each Subscription request from other users.
</CheckboxField>
<br /><br />
<button className="button is-primary" onClick={() => handleUpdate()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>
<span>Save</span>
</button>
<hr />
<nav className="level">
<div className="level-left">
<p className="level-item">
<button className="button is-primary" onClick={() => handleUpdate()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>
<span>Save</span>
</button>
</p>
</div>
<button className="button is-danger" onClick={() => handleLogout()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faDoorOpen} />
</span>
<span>Log Out</span>
</button>
<div className="level-right">
<p className="level-item">
<button className="button is-danger" onClick={() => handleLogout()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faDoorOpen} />
</span>
<span>Log Out</span>
</button>
</p>
</div>
</nav>
</div>
}

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

@ -2,7 +2,6 @@ import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Link, useParams, useHistory } from 'react-router-dom'
import classNames from 'classnames'
import moment from 'moment'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPlusSquare, faMinusSquare } from '@fortawesome/free-solid-svg-icons'
@ -12,7 +11,8 @@ import { getAuthenticatedUserId } from 'src/selectors/authentication'
import { getEntity } from 'src/selectors/entities'
import { getIsFetching } from 'src/selectors/requests'
import { setTitle } from 'src/utils'
import { useConfig } from 'src/hooks'
import { setTitle, urlForBlob } from 'src/utils'
import { AppState, AppThunkDispatch, EntityType, App, RequestKey, Installation } from 'src/types'
import { fetchInstallations } from 'src/actions/composer'
@ -20,6 +20,7 @@ import { getInstallations } from 'src/selectors/composer'
import PageHeader from 'src/components/page-header'
import Loading from 'src/components/pages/loading'
import AppInfo from 'src/components/app-info'
import { ClassDictionary } from 'src/types'
@ -34,6 +35,7 @@ const ViewApp: FC = () => {
const selfId = useSelector<AppState, string | undefined>(getAuthenticatedUserId)
const fetching = useSelector<AppState, boolean>(state => getIsFetching(state, RequestKey.InstallApp) || getIsFetching(state, RequestKey.UninstallApp))
const dispatch = useDispatch<AppThunkDispatch>()
const config = useConfig()
const history = useHistory()
useEffect(() => {
@ -59,7 +61,6 @@ const ViewApp: FC = () => {
'button': true,
'is-danger': installed,
'is-success': !installed,
'is-large': true,
'is-loading': fetching,
}
@ -94,55 +95,39 @@ const ViewApp: FC = () => {
}
}
const imageUrl = app.imageUrl ? urlForBlob(config, app.imageUrl) : undefined
return (
<div>
<PageHeader title={app.name} />
<div className="main-content">
<div className="centered-content">
<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>
<AppInfo app={app} />
{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="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="level-item has-text-centered">
<div>
<p className="heading">Updated</p>
<p className="title">{moment(app.updated).format('MMMM Do, YYYY')}</p>
<div className="media-content">
<div className="content">
<h1 className="is-size-3 has-text-primary">{app.name}</h1>
<br />
<p>{app.about}</p>
</div>
</div>
</nav>
<p>{app.about}</p>
</article>
<br />
{renderButton()}
{isCreator &&
<div>
<hr />
<Link className="button is-warning" to={`/a/${id}/edit`}>Edit App</Link>
</div>
}
<div className="buttons">
{renderButton()}
{isCreator && <Link className="button is-warning" to={`/a/${id}/edit`}>Edit App</Link>}
</div>
</div>
</div>
</div>

52
src/components/pages/group.tsx → src/components/pages/view-group.tsx

@ -2,13 +2,14 @@ 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 { faEdit, faUserCheck } from '@fortawesome/free-solid-svg-icons'
import { faEdit, faUserCheck, faBan } from '@fortawesome/free-solid-svg-icons'
import { handleApiError } from 'src/api/errors'
import { fetchGroup } from 'src/actions/groups'
import { getEntity } from 'src/selectors/entities'
import { setTitle } from 'src/utils'
import { useDeepCompareEffect, useConfig } from 'src/hooks'
import { setTitle, urlForBlob } from 'src/utils'
import { AppState, EntityType, Group, GroupMembershipType, AppThunkDispatch } from 'src/types'
import PageHeader from 'src/components/page-header'
@ -19,10 +20,11 @@ interface Params {
id: string
}
const GroupPage: FC = () => {
const ViewGroup: FC = () => {
const { id } = useParams<Params>()
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
const dispatch = useDispatch<AppThunkDispatch>()
const config = useConfig()
const history = useHistory()
useEffect(() => {
@ -33,13 +35,14 @@ const GroupPage: FC = () => {
}
}, [])
useEffect(() => {
useDeepCompareEffect(() => {
if (group) setTitle(group.name)
}, [group])
if (!group) return <Loading />
const isAdmin = group.membership === GroupMembershipType.Admin
const imageUrl = group.imageUrl ? urlForBlob(config, group.imageUrl) : undefined
return (
<div>
@ -49,10 +52,40 @@ const GroupPage: FC = () => {
<GroupInfo group={group} />
<div className="centered-content">
<p>{group.about}</p>
<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 has-text-primary">{group.name}</h1>
<br />
<p>{group.about}</p>
</div>
</div>
</article>
<br />
<div className="buttons">
<Link to={`/c/${group.id}/register`} className="button is-success">
<span className="icon is-small">
<FontAwesomeIcon icon={faUserCheck} />
</span>
<span>Create an Account</span>
</Link>
<button className="button is-danger">
<span className="icon is-small">
<FontAwesomeIcon icon={faBan} />
</span>
<span>Block</span>
</button>
{isAdmin &&
<Link to={`/c/${group.id}/admin/`} className="button is-warning">
<span className="icon is-small">
@ -61,13 +94,6 @@ const GroupPage: FC = () => {
<span>Edit {group.name}</span>
</Link>
}
<Link to={`/c/${group.id}/register`} className="button is-success">
<span className="icon is-small">
<FontAwesomeIcon icon={faUserCheck} />
</span>
<span>Create an Account</span>
</Link>
</div>
</div>
@ -77,4 +103,4 @@ const GroupPage: FC = () => {
)
}
export default GroupPage
export default ViewGroup

108
src/components/pages/view-user.tsx

@ -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

79
src/components/self-info.tsx

@ -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>
}
&nbsp;&nbsp;
<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

91
src/components/user-info.tsx

@ -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>
}
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>
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>
</article>
)
}
</div>
</nav>
)
export default UserInfo

5
src/reducers/config.ts

@ -6,6 +6,11 @@ import { ConfigState } from '../types'
const initialState: ConfigState = {
apiUrl: '',
blobUrl: '',
media: {
defaultMaxSize: 0,
coverMaxSize: 0,
iconMaxSize: 0,
}
}
const reducer: Reducer<ConfigState, ConfigActions> = (state = initialState, action) => {

9
src/types/config.ts

@ -0,0 +1,9 @@
export interface Config {
apiUrl: string
blobUrl: string
media: {
defaultMaxSize: number
coverMaxSize: number
iconMaxSize: number
}
}

3
src/types/entities.ts

@ -23,6 +23,9 @@ export type Group = Entity & {
name: string
membership?: GroupMembershipType
about: string
imageUrl: string
coverImageUrl: string
iconImageUrl: string
}
type BaseInstallation = Entity & {

1
src/types/index.ts

@ -30,6 +30,7 @@ export interface SasResponse {
id: string
}
export * from './config'
export * from './entities'
export * from './store'

7
src/types/store.ts

@ -1,10 +1,6 @@
import { EntityStore } from './entities'
export interface Config {
apiUrl: string
blobUrl: string
}
import { Config } from './config'
export enum NotificationType {
Info = 'info',
@ -29,6 +25,7 @@ export enum RequestKey {
FetchInstallations = 'fetch_installations',
FetchInvitations = 'fetch_invitations',
FetchSelfApps = 'fetch_self_apps',
FetchUser = 'fetch_user',
FetchUserAvailability = 'fetch_user_availability',
InstallApp = 'install_app',
UninstallApp = 'uninstall_app',

Loading…
Cancel
Save