Dwayne Harris 5 years ago
parent
commit
06149d5fd3
  1. 3
      .vscode/settings.json
  2. 2160
      package-lock.json
  3. 27
      package.json
  4. 21
      postcss-preset-env.d.ts
  5. 6
      src/actions/apps.ts
  6. 4
      src/actions/authentication.ts
  7. 13
      src/actions/menu.ts
  8. 8
      src/actions/registration.ts
  9. 24
      src/actions/theme.ts
  10. 44
      src/components/app-info.tsx
  11. 23
      src/components/app-list-item.tsx
  12. 50
      src/components/app.tsx
  13. 31
      src/components/composer.tsx
  14. 39
      src/components/controls/button.tsx
  15. 9
      src/components/controls/checkbox-field.tsx
  16. 0
      src/components/controls/cover-image-field.tsx
  17. 11
      src/components/controls/field-label.tsx
  18. 87
      src/components/controls/file-field.tsx
  19. 0
      src/components/controls/icon-image-field.tsx
  20. 0
      src/components/controls/image-field.tsx
  21. 108
      src/components/controls/password-field.tsx
  22. 28
      src/components/controls/primary-button.tsx
  23. 28
      src/components/controls/secondary-button.tsx
  24. 47
      src/components/controls/select-field.tsx
  25. 77
      src/components/controls/text-field.tsx
  26. 61
      src/components/controls/textarea-field.tsx
  27. 61
      src/components/controls/theme-field.tsx
  28. 21
      src/components/create-group-form.tsx
  29. 31
      src/components/create-group-step.tsx
  30. 23
      src/components/create-user-form.tsx
  31. 28
      src/components/create-user-step.tsx
  32. 21
      src/components/footer.tsx
  33. 26
      src/components/form-notification.tsx
  34. 103
      src/components/forms/password-field.tsx
  35. 71
      src/components/forms/text-field.tsx
  36. 57
      src/components/forms/textarea-field.tsx
  37. 49
      src/components/group-info.tsx
  38. 8
      src/components/group-invitations.tsx
  39. 4
      src/components/group-list-item.tsx
  40. 2
      src/components/group-logs.tsx
  41. 11
      src/components/help-text.tsx
  42. 9
      src/components/horizontal-rule.tsx
  43. 24
      src/components/level.tsx
  44. 16
      src/components/logo.tsx
  45. 27
      src/components/member-list-item.tsx
  46. 78
      src/components/navigation-menu.tsx
  47. 2
      src/components/notification-container.tsx
  48. 19
      src/components/notification.tsx
  49. 19
      src/components/page-header.tsx
  50. 9
      src/components/pages/about.tsx
  51. 13
      src/components/pages/apps.tsx
  52. 92
      src/components/pages/create-app.tsx
  53. 41
      src/components/pages/developers.tsx
  54. 138
      src/components/pages/edit-app.tsx
  55. 133
      src/components/pages/group-admin.tsx
  56. 28
      src/components/pages/groups.tsx
  57. 25
      src/components/pages/home.tsx
  58. 19
      src/components/pages/loading.tsx
  59. 42
      src/components/pages/login.tsx
  60. 29
      src/components/pages/register-group.tsx
  61. 16
      src/components/pages/register.tsx
  62. 153
      src/components/pages/self.tsx
  63. 122
      src/components/pages/view-app.tsx
  64. 127
      src/components/pages/view-group.tsx
  65. 48
      src/components/pages/view-post.tsx
  66. 177
      src/components/pages/view-user.tsx
  67. 8
      src/components/post-list.tsx
  68. 24
      src/components/post.tsx
  69. 16
      src/components/search.tsx
  70. 13
      src/components/section.tsx
  71. 92
      src/components/self-info.tsx
  72. 24
      src/components/spinner.tsx
  73. 9
      src/components/subtitle.tsx
  74. 6
      src/components/timeline.tsx
  75. 9
      src/components/title.tsx
  76. 42
      src/components/user-info.tsx
  77. 32
      src/components/user.tsx
  78. 5
      src/hooks/index.ts
  79. 22
      src/reducers/menu.ts
  80. 30
      src/reducers/theme.ts
  81. 3
      src/selectors/menu.ts
  82. 6
      src/selectors/theme.ts
  83. 4
      src/store/index.ts
  84. 367
      src/styles/app.css
  85. 207
      src/styles/app.scss
  86. 47
      src/styles/spinner.css
  87. 62
      src/styles/spinner.scss
  88. 78
      src/themes.ts
  89. 23
      src/types/index.ts
  90. 17
      src/types/store.ts
  91. 5
      src/utils/index.ts
  92. 22
      webpack.config.ts

3
.vscode/settings.json

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

2160
package-lock.json
File diff suppressed because it is too large
View File

27
package.json

@ -17,47 +17,44 @@
"deploy:prod": "run-s deploy:batch:prod deploy:config:prod"
},
"devDependencies": {
"@types/classnames": "^2.2.9",
"@types/html-webpack-plugin": "^3.2.1",
"@types/lodash": "^4.14.144",
"@types/mini-css-extract-plugin": "^0.8.0",
"@types/react": "^16.9.9",
"@types/react-dom": "^16.9.2",
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.3",
"@types/react-redux": "^7.1.5",
"@types/react-router-dom": "^5.1.0",
"@types/react-router-dom": "^5.1.2",
"@types/redux-logger": "^3.0.7",
"@types/uuid": "^3.4.5",
"@types/webpack": "^4.39.5",
"@types/uuid": "^3.4.6",
"@types/webpack": "^4.39.8",
"@types/webpack-bundle-analyzer": "^2.13.3",
"@types/webpack-dev-server": "^3.4.0",
"@types/zxcvbn": "^4.4.0",
"bulma": "^0.8.0",
"css-loader": "^3.2.0",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.12.0",
"npm-run-all": "^4.1.5",
"sass-loader": "^8.0.0",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"style-loader": "^1.0.0",
"ts-loader": "^6.2.0",
"ts-loader": "^6.2.1",
"ts-node": "^8.4.1",
"typescript": "^3.6.4",
"typescript": "^3.7.2",
"webpack": "^4.41.2",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.9",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0"
},
"dependencies": {
"@azure/storage-blob": "^10.5.0",
"@azure/identity": "^1.0.0",
"@azure/storage-blob": "^12.0.0",
"@fortawesome/fontawesome-common-types": "^0.2.25",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.7",
"classnames": "^2.2.6",
"history": "^4.10.1",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"re-reselect": "^3.4.0",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-redux": "^7.1.1",

21
postcss-preset-env.d.ts

@ -0,0 +1,21 @@
declare module 'postcss-preset-env' {
import {
plugin, Plugin, ParserInput,
Result, LazyResult, Root, ProcessOptions
} from 'postcss';
interface PluginOptions {
stage?: number;
features?: any;
browsers?: string;
insertBefore?: any;
insertAfter?: any;
autoprefixer?: any;
preserve?: boolean;
importFrom?: string;
exportTo?: string;
}
const PostcssPresetEnv: Plugin<PluginOptions>;
export default PostcssPresetEnv;
}

6
src/actions/apps.ts

@ -132,7 +132,7 @@ export const createApp = (options: AppOptions): AppThunkAction<string> => async
export const updateApp = (id: string, options: AppOptions): AppThunkAction => async dispatch => {
const { name, about, websiteUrl, companyName, version, composerUrl, rendererUrl, imageUrl, coverImageUrl, iconImageUrl } = options
dispatch(startRequest(RequestKey.CreateApp))
dispatch(startRequest(RequestKey.UpdateApp))
try {
await apiFetch({
@ -152,9 +152,9 @@ export const updateApp = (id: string, options: AppOptions): AppThunkAction => as
},
})
dispatch(finishRequest(RequestKey.CreateApp, true))
dispatch(finishRequest(RequestKey.UpdateApp, true))
} catch (err) {
dispatch(finishRequest(RequestKey.CreateApp, false))
dispatch(finishRequest(RequestKey.UpdateApp, false))
throw err
}
}

4
src/actions/authentication.ts

@ -114,10 +114,11 @@ interface UpdateSelfOptions {
privacy: string
imageUrl: string
coverImageUrl: string
theme: string
}
export const updateSelf = (options: UpdateSelfOptions): AppThunkAction => async dispatch => {
const { name, about, requiresApproval, privacy, imageUrl, coverImageUrl } = options
const { name, about, requiresApproval, privacy, imageUrl, coverImageUrl, theme } = options
dispatch(startRequest(RequestKey.UpdateSelf))
try {
@ -131,6 +132,7 @@ export const updateSelf = (options: UpdateSelfOptions): AppThunkAction => async
privacy,
imageUrl,
coverImageUrl,
theme,
},
})

13
src/actions/menu.ts

@ -1,13 +0,0 @@
import { Action } from 'redux'
export interface SetCollapsedAction extends Action {
type: 'MENU_SET_COLLAPSED'
payload: boolean
}
export type MenuActions = SetCollapsedAction
export const setCollapsed = (collapsed: boolean): SetCollapsedAction => ({
type: 'MENU_SET_COLLAPSED',
payload: collapsed
})

8
src/actions/registration.ts

@ -80,6 +80,7 @@ interface CreateGroupOptions {
imageUrl?: string
coverImageUrl?: string
iconImageUrl?: string
theme: string
}
interface CreateGroupResponse {
@ -87,7 +88,7 @@ interface CreateGroupResponse {
}
export const createGroup = (options: CreateGroupOptions): AppThunkAction<string> => async dispatch => {
const { name, registration, about, imageUrl, coverImageUrl, iconImageUrl } = options
const { name, registration, about, imageUrl, coverImageUrl, iconImageUrl, theme } = options
dispatch(startRequest(RequestKey.CreateGroup))
@ -102,6 +103,7 @@ export const createGroup = (options: CreateGroupOptions): AppThunkAction<string>
imageUrl,
coverImageUrl,
iconImageUrl,
theme,
},
})
@ -124,6 +126,7 @@ interface RegisterOptions {
requiresApproval: boolean
privacy: string
group?: string
theme: string
}
interface RegisterResponse {
@ -133,7 +136,7 @@ interface RegisterResponse {
}
export const register = (options: RegisterOptions): AppThunkAction<string> => async dispatch => {
const { id, email, password, name, imageUrl, coverImageUrl, requiresApproval, privacy, group } = options
const { id, email, password, name, imageUrl, coverImageUrl, requiresApproval, privacy, group, theme } = options
dispatch(startRequest(RequestKey.Register))
@ -151,6 +154,7 @@ export const register = (options: RegisterOptions): AppThunkAction<string> => as
requiresApproval,
privacy,
group,
theme,
},
})

24
src/actions/theme.ts

@ -0,0 +1,24 @@
import { Action } from 'redux'
import { ColorScheme } from '../types'
export interface SetThemeAction extends Action {
type: 'THEME_SET_THEME'
payload: string
}
export interface SetColorSchemeAction extends Action {
type: 'THEME_SET_COLOR_SCHEME'
payload: ColorScheme
}
export type ThemeActions = SetThemeAction | SetColorSchemeAction
export const setTheme = (name: string): SetThemeAction => ({
type: 'THEME_SET_THEME',
payload: name,
})
export const setColorScheme = (scheme: ColorScheme): SetColorSchemeAction => ({
type: 'THEME_SET_COLOR_SCHEME',
payload: scheme,
})

44
src/components/app-info.tsx

@ -1,44 +0,0 @@
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

23
src/components/app-list-item.tsx

@ -1,7 +1,7 @@
import React, { FC } from 'react'
import { Link } from 'react-router-dom'
import { useConfig } from 'src/hooks'
import { useConfig, useTheme } from 'src/hooks'
import { App } from 'src/types'
interface Props {
@ -9,24 +9,21 @@ interface Props {
}
const AppListItem: FC<Props> = ({ app }) => {
const theme = useTheme()
const config = useConfig()
return (
<article className="media">
<div className="app-list-item" style={{ backgroundColor: theme.backgroundPrimary, borderColor: theme.backgroundSecondary }}>
{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 className="image">
<img src={`${config.blobUrl}${app.imageUrl}`} />
</div>
}
<div>
<Link to={`/a/${app.id}`} style={{ color: theme.primary }}>{app.name}</Link>
{app.about && <p style={{ color: theme.text }}>{app.about}</p>}
</div>
</article>
</div>
)
}

50
src/components/app.tsx

@ -1,21 +1,23 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import { handleApiError } from 'src/api/errors'
import { fetchSelf, setChecked } from 'src/actions/authentication'
import { setConfig } from 'src/actions/config'
import { getFetching } from 'src/selectors'
import { getCollapsed } from 'src/selectors/menu'
import getConfig from 'src/config'
import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from 'src/constants'
import { useTheme } from 'src/hooks'
import { AppState, AppThunkDispatch } from 'src/types'
import Footer from './footer'
import Logo from './logo'
import NavigationMenu from './navigation-menu'
import NotificationContainer from './notification-container'
import Spinner from './spinner'
import Search from './search'
import SelfInfo from './self-info'
import About from './pages/about'
@ -35,18 +37,15 @@ import ViewGroup from './pages/view-group'
import ViewPost from './pages/view-post'
import ViewUser from './pages/view-user'
import '../styles/app.scss'
import '../styles/spinner.scss'
import '../styles/app.css'
import '../styles/spinner.css'
import { useDeepCompareEffect } from 'src/hooks'
const App: FC = () => {
const collapsed = useSelector<AppState, boolean>(getCollapsed)
const theme = useTheme()
const fetching = useSelector<AppState, boolean>(getFetching)
const dispatch = useDispatch<AppThunkDispatch>()
const mainMenuWidth = 275
const mainColumnMargin = collapsed ? 0 : mainMenuWidth
const init = async () => {
if (localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)) {
try {
@ -66,23 +65,18 @@ const App: FC = () => {
init()
}, [])
useDeepCompareEffect(() => {
document.body.style.backgroundColor = theme.backgroundPrimary
}, [theme])
return (
<Router>
<div>
<div id="main-menu" style={{ width: mainMenuWidth }}>
<div id="header">
<Link className="has-text-white is-size-3" to="/">Flexor</Link>
<hr className="has-background-grey-lighter" />
</div>
<NavigationMenu />
{fetching && <Spinner />}
<SelfInfo />
<Footer />
<main style={{ backgroundColor: theme.backgroundPrimary }}>
<div className="logo-container">
<Logo />
</div>
<div id="main-column" style={{ marginRight: mainColumnMargin }}>
<div className="content" style={{ borderLeftColor: theme.backgroundSecondary, borderRightColor: theme.backgroundPrimary }}>
<Switch>
<Route path="/c/:id/admin/:tab?">
<GroupAdmin />
@ -135,8 +129,18 @@ const App: FC = () => {
</Switch>
</div>
<div className="menu-container">
<div className="menu" style={{ backgroundColor: theme.backgroundSecondary, borderColor: theme.backgroundPrimary }}>
<Search />
<NavigationMenu />
{fetching && <Spinner color={theme.primary} />}
<SelfInfo />
<Footer />
</div>
</div>
<NotificationContainer />
</div>
</main>
</Router>
)
}

31
src/components/composer.tsx

@ -1,14 +1,14 @@
import React, { FC, useState, useEffect, useRef } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import { getOrigin } from 'src/utils'
import { useConfig, useDeepCompareEffect } from 'src/hooks'
import { useConfig, useDeepCompareEffect, useTheme } from 'src/hooks'
import { fetchInstallations, setSelectedInstallation, setHeight as setComposerHeight, setError as setComposerError } from 'src/actions/composer'
import { showNotification } from 'src/actions/notifications'
import { createPost } from 'src/actions/posts'
import { getInstallations, getSelectedInstallation, getError, getHeight as getComposerHeight } from 'src/selectors/composer'
import { AppState, Installation, ClassDictionary, AppThunkDispatch, NotificationType, Post } from 'src/types'
import { getColorScheme } from 'src/selectors/theme'
import { AppState, Installation, AppThunkDispatch, NotificationType, Post } from 'src/types'
import { IncomingMessageData, OutgoingMessageData } from 'src/types/communicator'
interface LimiterCollection {
@ -21,6 +21,8 @@ interface Props {
}
const Composer: FC<Props> = ({ parent, onPost }) => {
const theme = useTheme()
const colorScheme = useSelector<AppState, string>(getColorScheme)
const installations = useSelector<AppState, Installation[]>(getInstallations)
const installation = useSelector<AppState, Installation | undefined>(getSelectedInstallation)
const height = useSelector<AppState, number>(getComposerHeight)
@ -33,11 +35,6 @@ const Composer: FC<Props> = ({ parent, onPost }) => {
const composerUrl = installation ? installation.app.composerUrl : undefined
const showComposer = !!composerUrl && !error
const composerClasses: ClassDictionary = {
composer: true,
'composer-empty': !showComposer,
}
useEffect(() => {
dispatch(fetchInstallations())
}, [])
@ -101,6 +98,8 @@ const Composer: FC<Props> = ({ parent, onPost }) => {
content: {
installationId: installation.id,
settings: installation.settings,
theme,
colorScheme,
parent: parent ? {
text: parent.text,
cover: parent.cover,
@ -175,20 +174,20 @@ const Composer: FC<Props> = ({ parent, onPost }) => {
}
return (
<div className="container composer-container">
<div className={classNames(composerClasses)}>
<div className="composer-container" style={{ borderColor: theme.backgroundSecondary }}>
<div className="composer">
{showComposer &&
<iframe ref={ref} src={composerUrl} scrolling="yes" style={{ height, width: '100%', overflow: 'hidden' }} />
<iframe ref={ref} src={composerUrl} scrolling="yes" style={{ height }} />
}
{error && <span className="has-text-danger">Composer Error: {error}</span>}
{(!showComposer && !error) && <span>Choose an App.</span>}
{error && <div className="composer-error" style={{ backgroundColor: theme.backgroundSecondary, color: theme.red }}>Composer Error: {error}</div>}
{(!showComposer && !error) && <div className="composer-empty" style={{ backgroundColor: theme.backgroundSecondary, color: theme.secondary }}>Choose an App.</div>}
</div>
<div className="installations is-flex">
<div className="installations" style={{ backgroundColor: theme.backgroundPrimary }}>
{installations.map(i => (
<div key={i.id} className={installation && installation.id === i.id ? 'selected' : ''} onClick={() => handleClick(i.id)}>
<div key={i.id} className={installation && installation.id === i.id ? 'selected' : ''} style={{ borderColor: theme.backgroundSecondary }} onClick={() => handleClick(i.id)}>
<img src={`${config.blobUrl}${i.app.iconImageUrl}`} alt={i.app.name} style={{ width: 32 }} />
<p className="is-size-7 has-text-weight-bold">{i.app.name}</p>
<p style={{ color: theme.text }}>{i.app.name}</p>
</div>
))}
</div>

39
src/components/controls/button.tsx

@ -0,0 +1,39 @@
import React, { FC, MouseEventHandler } from 'react'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import Spinner from 'src/components/spinner'
export interface Props {
text: string
icon?: IconDefinition
loading?: boolean
color: string
backgroundColor: string
onClick?: MouseEventHandler
}
const Button: FC<Props> = ({ text, icon, loading, color, backgroundColor, onClick = noop }) => {
const isLoading = loading === undefined ? false : loading
const content = () => (
<>
{icon &&
<span className="icon">
<FontAwesomeIcon icon={icon} />
</span>
}
<span>{text}</span>
</>
)
return (
<button style={{ backgroundColor, color }} disabled={loading} onClick={onClick}>
{isLoading && <Spinner color={color} />}
{!isLoading && content()}
</button>
)
}
export default Button

9
src/components/forms/checkbox-field.tsx → src/components/controls/checkbox-field.tsx

@ -1,6 +1,6 @@
import React, { FC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useTheme } from 'src/hooks'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue } from 'src/selectors/forms'
import { AppState } from 'src/types'
@ -10,14 +10,15 @@ interface Props {
}
const CheckboxField: FC<Props> = ({ name, children }) => {
const theme = useTheme()
const value = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, name, false))
const dispatch = useDispatch()
return (
<label className="checkbox">
<input type="checkbox" checked={value} onChange={e => dispatch(setFieldValue(name, e.target.checked))} />
<label>
<input type="checkbox" checked={value} onChange={e => dispatch(setFieldValue(name, e.target.checked))} style={{ backgroundColor: theme.primary }} />
&nbsp;&nbsp;
{children}
<span style={{ color: theme.text }}>{children}</span>
</label>
)
}

0
src/components/forms/cover-image-field.tsx → src/components/controls/cover-image-field.tsx

11
src/components/controls/field-label.tsx

@ -0,0 +1,11 @@
import React, { FC } from 'react'
import { useTheme } from 'src/hooks'
const FieldLabel: FC = ({ children }) => {
const theme = useTheme()
return (
<label style={{ color: theme.secondary }}>{children}</label>
)
}
export default FieldLabel

87
src/components/forms/file-field.tsx → src/components/controls/file-field.tsx

@ -1,16 +1,18 @@
import React, { FC, ChangeEvent, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUpload } from '@fortawesome/free-solid-svg-icons'
import { uploadBrowserDataToBlockBlob, Aborter, BlockBlobURL, AnonymousCredential } from '@azure/storage-blob'
import { DefaultAzureCredential } from '@azure/identity'
import { BlockBlobClient } from '@azure/storage-blob'
import { useConfig } from 'src/hooks'
import { useConfig, useTheme } from 'src/hooks'
import { setFieldValue } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { getFieldValue } from 'src/selectors/forms'
import { apiFetch } from 'src/api/fetch'
import { AppState, ClassDictionary, SasResponse, NotificationType } from 'src/types'
import { AppState, SasResponse, NotificationType } from 'src/types'
import FieldLabel from 'src/components/controls/field-label'
interface Props {
name: string
@ -21,6 +23,7 @@ interface Props {
}
const FileField: FC<Props> = props => {
const theme = useTheme()
const value = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, props.name, false))
const config = useConfig()
const dispatch = useDispatch()
@ -31,12 +34,6 @@ const FileField: FC<Props> = props => {
const [uploading, setUploading] = useState(false)
const [uploaded, setUploaded] = useState(false)
const classes: ClassDictionary = {
file: true,
'is-primary': true,
'has-name': !!value,
}
const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
const file = event.target.files[0]
@ -50,16 +47,22 @@ const FileField: FC<Props> = props => {
const ext = file.name.substring(file.name.lastIndexOf('.'))
const { sas, id } = await apiFetch<SasResponse>({ path: '/api/sas' })
const filename = `${id}${ext}`
const blobURL = new BlockBlobURL(`${config.blobUrl}${filename}?${sas}`, BlockBlobURL.newPipeline(new AnonymousCredential()))
setUploading(true)
await uploadBrowserDataToBlockBlob(Aborter.none, file, blobURL, {
blockSize: 4 * 1024 * 1024,
progress: p => {
setProgress((p.loadedBytes / file.size) * 100)
}
})
const defaultAzureCredential = new DefaultAzureCredential()
const blockBlobClient = new BlockBlobClient(`${config.blobUrl}${filename}?${sas}`, defaultAzureCredential)
try {
await blockBlobClient.uploadBrowserData(file, {
onProgress: p => {
setProgress((p.loadedBytes / file.size) * 100)
}
})
} catch (err) {
console.error(err)
dispatch(showNotification(NotificationType.Error, `Upload error: ${err}`))
}
await apiFetch({
path: '/api/media',
@ -94,40 +97,38 @@ const FileField: FC<Props> = props => {
if (uploading) {
return (
<div className="field">
<label className="label">{label}</label>
<progress className="progress is-success" value={progress} max="100">{progress}%</progress>
<div>
<FieldLabel>{label}</FieldLabel>
<progress value={progress} max="100">{progress}%</progress>
</div>
)
}
return (
<div>
<div className="field">
<label className="label">{label}</label>
{value &&
<div style={{ padding: '10px 0px' }}>
<img src={`${config.blobUrl}${value}`} style={{ width: previewWidth }} />
<br />
<a className="is-danger is-size-7" onClick={() => handleDelete()}>Delete</a>
<div className="field">
<FieldLabel>{label}</FieldLabel>
{value &&
<div style={{ padding: '1rem 0px' }}>
<img src={`${config.blobUrl}${value}`} style={{ width: previewWidth }} />
<div style={{ color: theme.secondary, fontSize: '0.8rem' }}>
{value}
&nbsp;&nbsp;
(<a style={{ color: theme.red }} onClick={() => handleDelete()}>Delete</a>)
</div>
}
<div className={classNames(classes)}>
<label className="file-label">
<input className="file-input" type="file" name={name} onChange={handleChange} />
<span className="file-cta">
<span className="file-icon">
<FontAwesomeIcon icon={faUpload} />
</span>
<span className="file-label">
Upload
</span>
</span>
{value && <span className="file-name">{value}</span>}
</div>
}
{!value &&
<div style={{ padding: '1rem 0px' }}>
<label className="file-input" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<input type="file" name={name} onChange={handleChange} />
<div className="icon">
<FontAwesomeIcon icon={faUpload} />
</div>
<span>Choose a file...</span>
</label>
<p className="help" style={{ color: theme.text }}>{help}</p>
</div>
<p className="help">{help}</p>
</div>
}
</div>
)
}

0
src/components/forms/icon-image-field.tsx → src/components/controls/icon-image-field.tsx

0
src/components/forms/image-field.tsx → src/components/controls/image-field.tsx

108
src/components/controls/password-field.tsx

@ -0,0 +1,108 @@
import React, { FC, ReactNode } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import zxcvbn from 'zxcvbn'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { faKey, faExclamationTriangle, faCheckCircle } from '@fortawesome/free-solid-svg-icons'
import { useTheme } from 'src/hooks'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { AppState, FormNotification, NotificationType } from 'src/types'
import FieldLabel from 'src/components/controls/field-label'
import FormNotificationComponent from 'src/components/form-notification'
export interface Props {
placeholder?: string
userInputs?: string[]
showStrength?: boolean
}
const PasswordField: FC<Props> = ({
placeholder,
userInputs = [],
showStrength = true,
}) => {
const theme = useTheme()
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, 'password', ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, 'password'))
const dispatch = useDispatch()
let icon: IconDefinition | undefined
let passwordMessage: ReactNode | undefined
let successState = false
let errorState = false
if (value && showStrength) {
const { score } = zxcvbn(value, userInputs)
switch (score) {
case 0:
errorState = true
icon = faExclamationTriangle
passwordMessage = <span>Strength: <span style={{ color: theme.red }}>Unusable</span></span>
break
case 1:
errorState = true
passwordMessage = <span>Strength: <span style={{ color: theme.red }}>Not good</span></span>
break
case 2:
passwordMessage = <span>Strength: <span>OK</span></span>
break
case 3:
successState = true
passwordMessage = <span>Strength: <span style={{ color: theme.green }}>Good!</span></span>
break
case 4:
successState = true
icon = faCheckCircle
passwordMessage = <span>Strength: <span style={{ color: theme.green }}>LIT</span></span>
break
}
}
if (notification) {
switch (notification.type) {
case NotificationType.Success:
errorState = false
successState = true
case NotificationType.Error:
errorState = true
successState = false
}
}
const color = successState ? theme.green : errorState ? theme.red : theme.backgroundSecondary
const helpText = () => {
if (notification) return <FormNotificationComponent notification={notification} />
if (passwordMessage) return <p className="help">{passwordMessage}</p>
}
return (
<div className="field">
<FieldLabel>Password</FieldLabel>
<div className="control-container">
<div className="icon">
<FontAwesomeIcon icon={faKey} />
</div>
<div className="control">
<input
style={{ backgroundColor: theme.backgroundPrimary, borderColor: color, color: theme.text }}
type="password"
placeholder={placeholder}
value={value}
onChange={e => dispatch(setFieldValue('password', e.target.value))} />
</div>
{icon &&
<div className="icon">
<FontAwesomeIcon icon={icon} />
</div>
}
</div>
{helpText()}
</div>
)
}
export default PasswordField

28
src/components/controls/primary-button.tsx

@ -0,0 +1,28 @@
import React, { FC, MouseEventHandler } from 'react'
import noop from 'lodash/noop'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { useTheme } from 'src/hooks'
import Button from 'src/components/controls/button'
export interface Props {
text: string
icon?: IconDefinition
loading?: boolean
onClick?: MouseEventHandler
}
const PrimaryButton: FC<Props> = ({ text, icon, loading, onClick = noop }) => {
const theme = useTheme()
return (
<Button
text={text}
icon={icon}
loading={loading}
color={theme.primaryAlternate}
backgroundColor={theme.primary}
onClick={onClick} />
)
}
export default PrimaryButton

28
src/components/controls/secondary-button.tsx

@ -0,0 +1,28 @@
import React, { FC, MouseEventHandler } from 'react'
import noop from 'lodash/noop'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { useTheme } from 'src/hooks'
import Button from 'src/components/controls/button'
export interface Props {
text: string
icon?: IconDefinition
loading?: boolean
onClick?: MouseEventHandler
}
const PrimaryButton: FC<Props> = ({ text, icon, loading, onClick = noop }) => {
const theme = useTheme()
return (
<Button
text={text}
icon={icon}
loading={loading}
color={theme.backgroundSecondary}
backgroundColor={theme.secondary}
onClick={onClick} />
)
}
export default PrimaryButton

47
src/components/forms/select-field.tsx → src/components/controls/select-field.tsx

@ -1,13 +1,14 @@
import React, { FC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'
import { useTheme } from 'src/hooks'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { notificationTypeToClassName } from 'src/utils'
import { AppState, FormNotification, ClassDictionary } from 'src/types'
import { AppState, FormNotification } from 'src/types'
import FieldLabel from 'src/components/controls/field-label'
import FormNotificationComponent from 'src/components/form-notification'
interface SelectOptions {
[value: string]: string
@ -26,44 +27,32 @@ const SelectField: FC<Props> = ({
options,
icon,
}) => {
const theme = useTheme()
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name))
const dispatch = useDispatch()
const opts = Object.entries(options)
const controlClassDictionary: ClassDictionary = { control: true }
const helpClassDictionary: ClassDictionary = { help: true }
if (notification) {
const ncn = notificationTypeToClassName(notification.type)
controlClassDictionary[ncn] = true
helpClassDictionary[ncn] = true
}
if (icon) {
controlClassDictionary['has-icons-left'] = true
}
return (
<div className="field">
<label className="label">{label}</label>
<div className={classNames(controlClassDictionary)}>
<div className="select">
<select value={value} onChange={e => dispatch(setFieldValue(name, e.target.value))}>
{opts.map(([key, value]) => <option key={key} value={key}>{value}</option>)}
</select>
</div>
<FieldLabel>{label}</FieldLabel>
<div className="control-container">
{icon &&
<div className="icon is-small is-left">
<div className="icon" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<FontAwesomeIcon icon={icon} />
</div>
}
<div className="control">
<select
style={{ backgroundColor: theme.backgroundSecondary, borderColor: theme.secondary, color: theme.text }}
value={value}
onChange={e => dispatch(setFieldValue(name, e.target.value))}>
{opts.map(([key, value]) => <option key={key} value={key}>{value}</option>)}
</select>
</div>
</div>
{notification &&
<p className={classNames(helpClassDictionary)}>{notification.message}</p>
}
{notification && <FormNotificationComponent notification={notification} />}
</div>
)
}

77
src/components/controls/text-field.tsx

@ -0,0 +1,77 @@
import React, { FC, FocusEventHandler } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { useTheme } from 'src/hooks'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { AppState, FormNotification, NotificationType } from 'src/types'
import FieldLabel from 'src/components/controls/field-label'
import FormNotificationComponent from 'src/components/form-notification'
import HelpText from 'src/components/help-text'
interface Props {
name: string
label: string
type?: 'text' | 'email'
placeholder?: string
help?: string
icon?: IconDefinition
onBlur?: FocusEventHandler<HTMLInputElement>
}
const TextField: FC<Props> = ({
name,
label,
type = 'text',
placeholder,
help,
icon,
onBlur = noop,
}) => {
const theme = useTheme()
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name))
const dispatch = useDispatch()
let color = theme.secondary
if (notification) {
switch (notification.type) {
case NotificationType.Error:
color = theme.red
break
case NotificationType.Success:
color = theme.green
break
}
}
return (
<div className="field">
<FieldLabel>{label}</FieldLabel>
<div className="control-container">
{icon &&
<div className="icon" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<FontAwesomeIcon icon={icon} />
</div>
}
<div className="control">
<input
style={{ backgroundColor: theme.backgroundSecondary, borderColor: color, color: theme.text }}
type={type}
placeholder={placeholder}
value={value}
onChange={e => dispatch(setFieldValue(name, e.target.value))}
onBlur={onBlur} />
</div>
</div>
{(!notification && help) && <HelpText>{help}</HelpText>}
{notification && <FormNotificationComponent notification={notification} />}
</div>
)
}
export default TextField

61
src/components/controls/textarea-field.tsx

@ -0,0 +1,61 @@
import React, { FC, FocusEventHandler } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import noop from 'lodash/noop'
import { useTheme } from 'src/hooks'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { AppState, FormNotification, NotificationType } from 'src/types'
import FieldLabel from 'src/components/controls/field-label'
import FormNotificationComponent from 'src/components/form-notification'
export interface Props {
name: string
label: string
placeholder?: string
onBlur?: FocusEventHandler
}
const TextField: FC<Props> = ({
name,
label,
placeholder,
onBlur = noop,
}) => {
const theme = useTheme()
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name))
const dispatch = useDispatch()
let color = theme.secondary
if (notification) {
switch (notification.type) {
case NotificationType.Error:
color = theme.red
break
case NotificationType.Success:
color = theme.green
break
}
}
return (
<div className="field">
<FieldLabel>{label}</FieldLabel>
<div className="control-container">
<div className="control">
<textarea
style={{ backgroundColor: theme.backgroundSecondary, borderColor: color, color: theme.text }}
placeholder={placeholder}
value={value}
onChange={e => dispatch(setFieldValue(name, e.target.value))}
onBlur={onBlur} />
</div>
</div>
{notification && <FormNotificationComponent notification={notification} />}
</div>
)
}
export default TextField

61
src/components/controls/theme-field.tsx

@ -0,0 +1,61 @@
import React, { FC, useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import capitalize from 'lodash/capitalize'
import { useTheme } from 'src/hooks'
import { setFieldValue } from 'src/actions/forms'
import { setTheme } from 'src/actions/theme'
import { getFieldValue } from 'src/selectors/forms'
import { getThemeName } from 'src/selectors/theme'
import { AppState, Theme, ColorScheme } from 'src/types'
import FieldLabel from 'src/components/controls/field-label'
import themes from 'src/themes'
export interface Props {
name: string
label: string
}
const ThemeField: FC<Props> = ({ name, label }) => {
const currentTheme = useTheme()
const currentThemeName = useSelector<AppState, string>(getThemeName)
const [previousThemeName, setPreviousThemeName] = useState('')
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, ''))
const dispatch = useDispatch()
const themeList = Object.entries(themes).map(([name, schemes]) => {
return [name, schemes[ColorScheme.Light]] as [string, Theme]
})
const handleMouseEnter = (name: string) => {
dispatch(setTheme(name))
}
const handleMouseLeave = () => {
dispatch(setTheme(previousThemeName))
}
useEffect(() => {
setPreviousThemeName(currentThemeName)
}, [])
return (
<div className="field">
<FieldLabel>{label}</FieldLabel>
<div className="control">
<div className="theme-picker">
{themeList.map(([themeName, theme]) => (
<div
style={{ backgroundColor: theme.primary, borderColor: themeName === value ? currentTheme.red : currentTheme.secondary }}
onMouseEnter={() => handleMouseEnter(themeName)}
onMouseLeave={() => handleMouseLeave()}
onClick={() => dispatch(setFieldValue(name, themeName))}></div>
))}
</div>
<div style={{ color: currentTheme.primary }}>{capitalize(currentThemeName)}</div>
</div>
</div>
)
}
export default ThemeField

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

@ -5,12 +5,13 @@ 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 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'
import CheckboxField from 'src/components/controls/checkbox-field'
import TextField from 'src/components/controls/text-field'
import SelectField from 'src/components/controls/select-field'
import ImageField from 'src/components/controls/image-field'
import CoverImageField from 'src/components/controls/cover-image-field'
import IconImageField from 'src/components/controls/icon-image-field'
import ThemeField from 'src/components/controls/theme-field'
const CreateGroupForm: FC = () => {
const dispatch = useDispatch()
@ -28,17 +29,13 @@ const CreateGroupForm: FC = () => {
}
return (
<div className="container">
<div>
<TextField name="group-name" label="Community Name" onBlur={e => checkAvailability(e.target.value)} />
<br />
<SelectField name="group-registration" label="Registration" options={registrationOptions} icon={faIdCard} />
<br />
<ThemeField name="group-theme" label="Color" />
<ImageField name="group-image" label="Community Image" />
<br />
<CoverImageField name="group-cover-image" />
<br />
<IconImageField name="group-icon-image" />
<br />
<CheckboxField name="group-agree">
I agree to the Communities <Link to="/terms/communities">terms and conditions</Link>.
</CheckboxField>

31
src/components/create-group-step.tsx

@ -1,18 +1,20 @@
import React, { FC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBuilding, faArrowLeft } from '@fortawesome/free-solid-svg-icons'
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'
import { setFieldNotification } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { setStep } from 'src/actions/registration'
import { getForm } from 'src/selectors/forms'
import { valueFromForm } from 'src/utils'
import { MAX_ID_LENGTH } from 'src/constants'
import { AppState, AppThunkDispatch, NotificationType, Form } from 'src/types'
import CreateGroupForm from './create-group-form'
import { valueFromForm } from 'src/utils'
import PrimaryButton from 'src/components/controls/primary-button'
import SecondaryButton from 'src/components/controls/secondary-button'
interface Props {
register: () => void
@ -49,32 +51,17 @@ const CreateGroupStep: FC<Props> = ({ register }) => {
}
return (
<div className="centered-content" style={{ maxWidth: 800 }}>
<div className="centered-content-icon has-background-primary">
<span className="icon is-large has-text-white">
<FontAwesomeIcon icon={faBuilding} size="2x" />
</span>
</div>
<div>
<CreateGroupForm />
<hr />
<nav className="level">
<div className="level-left">
<p className="level-item">
<button className="button" onClick={() => dispatch(setStep(0))}>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowLeft} />
</span>
<span>Your Account</span>
</button>
</p>
<div>
<SecondaryButton text="Your Account" icon={faArrowLeft} onClick={() => dispatch(setStep(0))} />
</div>
<div className="level-right">
<p className="level-item">
<button className="button is-success" onClick={() => next()}>Finish</button>
</p>
<div>
<PrimaryButton text="Finish" onClick={() => next()} />
</div>
</nav>
</div>

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

@ -5,12 +5,13 @@ import { faEnvelope, faIdCard, faUserShield } from '@fortawesome/free-solid-svg-
import { checkUserAvailability } from 'src/actions/registration'
import { PRIVACY_OPTIONS } from 'src/constants'
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 ImageField from 'src/components/forms/image-field'
import CoverImageField from 'src/components/forms/cover-image-field'
import CheckboxField from 'src/components/controls/checkbox-field'
import TextField from 'src/components/controls/text-field'
import PasswordField from 'src/components/controls/password-field'
import SelectField from 'src/components/controls/select-field'
import ImageField from 'src/components/controls/image-field'
import CoverImageField from 'src/components/controls/cover-image-field'
import ThemeField from 'src/components/controls/theme-field'
const CreateUserForm: FC = () => {
const dispatch = useDispatch()
@ -22,21 +23,15 @@ const CreateUserForm: FC = () => {
}
return (
<div className="container">
<div>
<TextField icon={faIdCard} name="user-id" label="Username" placeholder="Your Username/ID" onBlur={e => checkAvailability(e.target.value)} />
<br />
<TextField name="user-name" label="Display Name" placeholder="Whatever you want to go by" />
<br />
<TextField type="email" icon={faEnvelope} name="user-email" label="Email Address" placeholder="Your email address" />
<br />
<PasswordField placeholder="Your new password" />
<br />
<ThemeField name="user-theme" label="Color" />
<ImageField name="user-image" label="Avatar" />
<br />
<CoverImageField name="user-cover-image" />
<br />
<SelectField name="user-privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<br />
<CheckboxField name="user-requires-approval">
Approve each Subscription request from other users.
</CheckboxField>

28
src/components/create-user-step.tsx

@ -2,18 +2,19 @@ import React, { FC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import zxcvbn from 'zxcvbn'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser, faArrowRight } from '@fortawesome/free-solid-svg-icons'
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
import { setFieldNotification } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { setStep } from 'src/actions/registration'
import { getForm, getFieldValue } from 'src/selectors/forms'
import { getForm } from 'src/selectors/forms'
import { MAX_ID_LENGTH, MAX_NAME_LENGTH } from 'src/constants'
import { valueFromForm } from 'src/utils'
import { AppState, AppThunkDispatch, NotificationType, Form } from 'src/types'
import CreateUserForm from './create-user-form'
import PrimaryButton from 'src/components/controls/primary-button'
const CreateUserStep: FC = () => {
const form = useSelector<AppState, Form>(getForm)
@ -71,30 +72,13 @@ const CreateUserStep: FC = () => {
}
return (
<div className="centered-content" style={{ maxWidth: 800 }}>
<div className="centered-content-icon has-background-primary">
<span className="icon is-large has-text-white">
<FontAwesomeIcon icon={faUser} size="2x" />
</span>
</div>
<div>
<CreateUserForm />
<hr />
<nav className="level">
<div className="level-left">
<p className="level-item"></p>
</div>
<div className="level-right">
<p className="level-item">
<button className="button is-success" onClick={() => next()}>
<span>Community</span>
<span className="icon is-small">
<FontAwesomeIcon icon={faArrowRight} />
</span>
</button>
</p>
<div>
<PrimaryButton text="Community" onClick={() => next()} />
</div>
</nav>
</div>

21
src/components/footer.tsx

@ -1,20 +1,23 @@
import React, { FC } from 'react'
import { Link } from 'react-router-dom'
import { useTheme } from 'src/hooks'
const Divider: FC = () => <>&nbsp;&nbsp;&#9900;&nbsp;&nbsp;</>
const Footer: FC = () => (
<footer>
<div className="content has-text-centered has-text-white is-size-7">
<Link className="has-text-white is-inline-block" to="/">Home</Link>
const Footer: FC = () => {
const theme = useTheme()
return (
<footer style={{ color: theme.text }}>
<Link style={{ color: theme.primary }} to="/">Home</Link>
<Divider />
<Link className="has-text-white is-inline-block" to="/developers">Developers</Link>
<Link style={{ color: theme.primary }} to="/developers">Developers</Link>
<Divider />
<Link className="has-text-white is-inline-block" to="/about">About</Link>
<Link style={{ color: theme.primary }} to="/about">About</Link>
<p>&copy; 2019 Flexor.cc</p>
</div>
</footer>
)
</footer>
)
}
export default Footer

26
src/components/form-notification.tsx

@ -0,0 +1,26 @@
import React, { FC } from 'react'
import { useTheme } from 'src/hooks'
import { NotificationType, FormNotification } from 'src/types'
interface Props {
notification: FormNotification
}
const Notification: FC<Props> = ({ notification }) => {
const theme = useTheme()
let color = theme.text
switch (notification.type) {
case NotificationType.Error:
color = theme.red
break
case NotificationType.Success:
color = theme.green
}
return (
<p className="help" style={{ color }}>{notification.message}</p>
)
}
export default Notification

103
src/components/forms/password-field.tsx

@ -1,103 +0,0 @@
import React, { FC, ReactNode } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import zxcvbn from 'zxcvbn'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { faKey, faExclamationTriangle, faCheckCircle } from '@fortawesome/free-solid-svg-icons'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { notificationTypeToClassName } from 'src/utils'
import { AppState, FormNotification, ClassDictionary } from 'src/types'
export interface Props {
placeholder?: string
userInputs?: string[]
showStrength?: boolean
}
const PasswordField: FC<Props> = ({
placeholder,
userInputs = [],
showStrength = true,
}) => {
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, 'password', ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, 'password'))
const dispatch = useDispatch()
const inputClassDictionary: ClassDictionary = { input: true }
const controlClassDictionary: ClassDictionary = { control: true, 'has-icons-left': true }
const helpClassDictionary: ClassDictionary = { help: true }
let icon: IconDefinition | undefined
let passwordMessage: ReactNode | undefined
if (value && showStrength) {
const { score } = zxcvbn(value, userInputs)
switch (score) {
case 0:
inputClassDictionary['is-danger'] = true
controlClassDictionary['has-icons-right'] = true
icon = faExclamationTriangle
passwordMessage = <span>Strength: <span className="has-text-danger">Unusable</span></span>
break
case 1:
inputClassDictionary['is-danger'] = true
passwordMessage = <span>Strength: <span className="has-text-danger">Not good</span></span>
break
case 2:
inputClassDictionary['is-warning'] = true
passwordMessage = <span>Strength: <span className="has-text-warning">OK</span></span>
break
case 3:
inputClassDictionary['is-success'] = true
passwordMessage = <span>Strength: <span className="has-text-success">Good!</span></span>
break
case 4:
inputClassDictionary['is-success'] = true
controlClassDictionary['has-icons-right'] = true
icon = faCheckCircle
passwordMessage = <span>Strength: <span className="has-text-success">LIT</span></span>
break
}
}
if (notification) {
const ncn = notificationTypeToClassName(notification.type)
helpClassDictionary[ncn] = true
inputClassDictionary[ncn] = true
}
const helpText = () => {
if (notification) return <p className={classNames(helpClassDictionary)}>{notification.message}</p>
if (passwordMessage) return <p className="help">{passwordMessage}</p>
}
return (
<div className="field">
<label className="label">Password</label>
<div className={classNames(controlClassDictionary)}>
<input
className={classNames(inputClassDictionary)}
type="password"
placeholder={placeholder}
value={value}
onChange={e => dispatch(setFieldValue('password', e.target.value))} />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faKey} />
</span>
{icon &&
<span className="icon is-small is-right">
<FontAwesomeIcon icon={icon} />
</span>
}
</div>
{helpText()}
</div>
)
}
export default PasswordField

71
src/components/forms/text-field.tsx

@ -1,71 +0,0 @@
import React, { FC, FocusEventHandler } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import noop from 'lodash/noop'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { notificationTypeToClassName } from 'src/utils'
import { AppState, FormNotification, ClassDictionary } from 'src/types'
interface Props {
name: string
label: string
type?: 'text' | 'email'
placeholder?: string
help?: string
icon?: IconDefinition
onBlur?: FocusEventHandler<HTMLInputElement>
}
const TextField: FC<Props> = ({
name,
label,
type = 'text',
placeholder,
help,
icon,
onBlur = noop,
}) => {
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name))
const dispatch = useDispatch()
const controlClassDictionary = { control: true, 'has-icons-left': !!icon }
const helpClassDictionary: ClassDictionary = { help: true }
const inputClassDictionary: ClassDictionary = { input: true }
if (notification) {
const ncn = notificationTypeToClassName(notification.type)
helpClassDictionary[ncn] = true
inputClassDictionary[ncn] = true
}
return (
<div className="field">
<label className="label">{label}</label>
<div className={classNames(controlClassDictionary)}>
<input
className={classNames(inputClassDictionary)}
type={type}
placeholder={placeholder}
value={value}
onChange={e => dispatch(setFieldValue(name, e.target.value))}
onBlur={onBlur} />
{icon &&
<span className="icon is-small is-left">
<FontAwesomeIcon icon={icon} />
</span>
}
</div>
{(!notification && help) && <p className="help">{help}</p>}
{notification && <p className={classNames(helpClassDictionary)}>{notification.message}</p>}
</div>
)
}
export default TextField

57
src/components/forms/textarea-field.tsx

@ -1,57 +0,0 @@
import React, { FC, FocusEventHandler } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'
import noop from 'lodash/noop'
import { setFieldValue } from 'src/actions/forms'
import { getFieldValue, getFieldNotification } from 'src/selectors/forms'
import { notificationTypeToClassName } from 'src/utils'
import { AppState, FormNotification, ClassDictionary } from 'src/types'
export interface Props {
name: string
label: string
placeholder?: string
onBlur?: FocusEventHandler
}
const TextField: FC<Props> = ({
name,
label,
placeholder,
onBlur = noop,
}) => {
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, ''))
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name))
const dispatch = useDispatch()
const helpClassDictionary: ClassDictionary = { help: true }
const inputClassDictionary: ClassDictionary = { textarea: true }
if (notification) {
const ncn = notificationTypeToClassName(notification.type)
helpClassDictionary[ncn] = true
inputClassDictionary[ncn] = true
}
return (
<div className="field">
<label className="label">{label}</label>
<div className="control">
<textarea
className={classNames(inputClassDictionary)}
placeholder={placeholder}
value={value}
onChange={e => dispatch(setFieldValue(name, e.target.value))}
onBlur={onBlur} />
</div>
{notification &&
<p className={classNames(helpClassDictionary)}>{notification.message}</p>
}
</div>
)
}
export default TextField

49
src/components/group-info.tsx

@ -1,49 +0,0 @@
import React, { FC } from 'react'
import moment from 'moment'
import { Group } from 'src/types'
interface Props {
group: Group
}
const GroupInfo: FC<Props> = ({ group }) => (
<nav className="level">
<div className="level-item has-text-centered">
<div>
<p className="heading">Members</p>
<p className="title">{group.members}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Posts</p>
<p className="title">{group.posts}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading has-text-success">Awards</p>
<p className="title">{group.posts}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Points</p>
<p className="title">{group.points}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Created</p>
<p className="title is-size-5">{moment(group.created).format('MMMM Do, YYYY')}</p>
</div>
</div>
</nav>
)
export default GroupInfo

8
src/components/group-invitations.tsx

@ -12,7 +12,9 @@ import { getFieldValue } from 'src/selectors/forms'
import { AppState, Invitation, AppThunkDispatch } from 'src/types'
import SelectField from 'src/components/forms/select-field'
import Title from 'src/components/title'
import Subtitle from 'src/components/subtitle'
import SelectField from 'src/components/controls/select-field'
interface Props {
group: string
@ -61,8 +63,8 @@ const GroupInvitations: FC<Props> = ({ group }) => {
return (
<div>
<h1 className="title is-size-4">Invitations</h1>
<h2 className="subtitle is-size-6">Create an invitation for someone to create a new account in this Community.</h2>
<Title>Invitations</Title>
<Subtitle>Create an invitation for someone to create a new account in this Community.</Subtitle>
<div className="invitation-options">
<SelectField name="expiration" label="Expires" options={expirationOptions} icon={faStopwatch} />

4
src/components/group-list-item.tsx

@ -3,8 +3,6 @@ import { Link } from 'react-router-dom'
import { Group } from 'src/types'
import GroupInfo from 'src/components/group-info'
interface Props {
group: Group
}
@ -15,8 +13,6 @@ const GroupListItem: FC<Props> = ({ group }) => (
{group.about && <p>{group.about}</p>}
<br /><br />
<GroupInfo group={group} />
</div>
)

2
src/components/group-logs.tsx

@ -27,7 +27,7 @@ const MemberList: FC<Props> = ({ group }) => {
}, [group])
return (
<table className="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
<table>
<thead>
<tr>
<th>Who</th>

11
src/components/help-text.tsx

@ -0,0 +1,11 @@
import React, { FC } from 'react'
import { useTheme } from 'src/hooks'
const Notification: FC = ({ children }) => {
const theme = useTheme()
return (
<p className="help" style={{ color: theme.secondary }}>{children}</p>
)
}
export default Notification

9
src/components/horizontal-rule.tsx

@ -0,0 +1,9 @@
import React, { FC } from 'react'
import { useTheme } from 'src/hooks'
const HorizontalRule: FC = () => {
const theme = useTheme()
return <hr style={{ borderColor: theme.backgroundSecondary, color: theme.backgroundSecondary }} />
}
export default HorizontalRule

24
src/components/level.tsx

@ -0,0 +1,24 @@
import React, { FC } from 'react'
import { useTheme } from 'src/hooks'
import { LevelItem } from 'src/types'
interface Props {
items: LevelItem[]
}
const Level: FC<Props> = ({ items }) => {
const theme = useTheme()
return (
<nav className="level">
{items.map(item => (
<div>
{item.label && <p className="label" style={{ color: theme.secondary }}>{item.label}</p>}
<p className="content" style={{ color: theme.text }}>{item.content}</p>
</div>
))}
</nav>
)
}
export default Level

16
src/components/logo.tsx

@ -0,0 +1,16 @@
import React, { FC } from 'react'
import { useHistory } from 'react-router-dom'
import { useTheme } from 'src/hooks'
const Logo: FC = () => {
const theme = useTheme()
const history = useHistory()
return (
<div className="logo" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate, cursor: 'pointer' }} onClick={() => history.push('/')}>
F
</div>
)
}
export default Logo

27
src/components/member-list-item.tsx

@ -1,35 +1,32 @@
import React, { FC } from 'react'
import { Link } from 'react-router-dom'
import classNames from 'classnames'
import capitalize from 'lodash/capitalize'
import { User, GroupMembershipType, ClassDictionary } from 'src/types'
import { useTheme } from 'src/hooks'
import { User, GroupMembershipType } from 'src/types'
interface Props {
member: User
}
const MemberListItem: FC<Props> = ({ member }) => {
const tagClass = () => {
const theme = useTheme()
const tagColor = () => {
switch (member.membership as GroupMembershipType) {
case GroupMembershipType.Admin: return 'is-success'
case GroupMembershipType.Moderator: return 'is-warning'
case GroupMembershipType.Member: return 'is-info'
case GroupMembershipType.Admin: return theme.green
case GroupMembershipType.Moderator: return theme.red
case GroupMembershipType.Member: return theme.blue
default: return ''
}
}
const tagClassDictionary: ClassDictionary = {
tag: true,
[tagClass()]: true,
}
return (
<div className="member">
<Link to={`/u/${member.id}`} className="is-size-5">{member.name}</Link>
<Link to={`/u/${member.id}`} style={{ color: theme.primary, fontSize: '1.1rem' }}>{member.name}</Link>
<br />
<Link to={`/u/${member.id}`} className="is-size-6 is-red">@{member.id}</Link>
<Link to={`/u/${member.id}`} style={{ color: theme.secondary, fontSize: '0.9rem' }}>@{member.id}</Link>
<br />
<span className={classNames(tagClassDictionary)}>{capitalize(member.membership as string)}</span>
<span className="tag" style={{ color: tagColor() }}>{capitalize(member.membership as string)}</span>
</div>
)
}

78
src/components/navigation-menu.tsx

@ -1,26 +1,66 @@
import React, { FC } from 'react'
import { Link } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faStream, faPaperPlane } from '@fortawesome/free-solid-svg-icons'
import { faStream, faPaperPlane, faSun, faMoon } from '@fortawesome/free-solid-svg-icons'
import { useTheme } from 'src/hooks'
import { setColorScheme } from 'src/actions/theme'
import { getColorScheme } from 'src/selectors/theme'
import { AppState, ColorScheme } from 'src/types'
const NavigationMenu: FC = () => (
<div id="navigation">
<div>
<span className="icon has-text-white">
<FontAwesomeIcon icon={faStream} />
</span>
&nbsp;
<Link className="has-text-white" to="/">Timeline</Link>
</div>
const NavigationMenu: FC = () => {
const theme = useTheme()
const scheme = useSelector<AppState, ColorScheme>(getColorScheme)
const dispatch = useDispatch()
<div>
<span className="icon has-text-white">
<FontAwesomeIcon icon={faPaperPlane} />
</span>
&nbsp;
<Link className="has-text-white" to="/apps">Apps</Link>
</div>
</div>
)
const switchColorSchemeItem = () => {
switch (scheme) {
case ColorScheme.Light:
return (
<a style={{ color: theme.primary, cursor: 'pointer' }} onClick={() => dispatch(setColorScheme(ColorScheme.Dark))}>
<span className="icon" style={{ color: theme.primary }}>
<FontAwesomeIcon icon={faMoon} />
</span>
&nbsp;
<span>Dark Mode</span>
</a>
)
case ColorScheme.Dark:
return (
<a style={{ color: theme.primary, cursor: 'pointer' }} onClick={() => dispatch(setColorScheme(ColorScheme.Light))}>
<span className="icon" style={{ color: theme.primary }}>
<FontAwesomeIcon icon={faSun} />
</span>
&nbsp;
<span>Light Mode</span>
</a>
)
}
}
return (
<nav>
<div>
<span className="icon" style={{ color: theme.primary }}>
<FontAwesomeIcon icon={faStream} />
</span>
&nbsp;
<Link style={{ color: theme.primary }} to="/">Timeline</Link>
</div>
<div>
<span className="icon" style={{ color: theme.primary }}>
<FontAwesomeIcon icon={faPaperPlane} />
</span>
&nbsp;
<Link style={{ color: theme.primary }} to="/apps">Apps</Link>
</div>
<div>
{switchColorSchemeItem()}
</div>
</nav>
)
}
export default NavigationMenu

2
src/components/notification-container.tsx

@ -22,7 +22,7 @@ const NotificationContainer: FC = () => {
}
return (
<div id="notification-container">
<div className="notification-container">
{notifications.map(notification => {
const content = () => {
switch (notification.type) {

19
src/components/notification.tsx

@ -1,7 +1,5 @@
import React, { FC, MouseEventHandler } from 'react'
import classNames from 'classnames'
import { notificationTypeToClassName } from 'src/utils'
import { useTheme } from 'src/hooks'
import { NotificationType } from 'src/types'
interface Props {
@ -13,18 +11,23 @@ interface Props {
}
const Notification: FC<Props> = ({ id, type, auto, setAuto, dismiss, children }) => {
const classnames = classNames({
notification: true,
[notificationTypeToClassName(type)]: true,
})
const theme = useTheme()
const handleDismiss: MouseEventHandler = e => {
e.stopPropagation()
dismiss(id)
}
const color = () => {
switch (type) {
case NotificationType.Success: return theme.green
case NotificationType.Error: return theme.red
default: return theme.blue
}
}
return (
<div className={classnames} onClick={() => setAuto(id)}>
<div className="notification" onClick={() => setAuto(id)} style={{ backgroundColor: color(), color: theme.text }}>
{!auto && <button className="delete" onClick={handleDismiss}></button>}
{children}
</div>

19
src/components/page-header.tsx

@ -1,19 +0,0 @@
import React, { FC } from 'react'
interface Props {
title: string
subtitle?: string
}
const PageHeader: FC<Props> = ({ title, subtitle }) => (
<section className="hero is-dark is-bold">
<div className="hero-body">
<div className="container">
<h1 className="title">{title}</h1>
{subtitle && <h2 className="subtitle">{subtitle}</h2>}
</div>
</div>
</section>
)
export default PageHeader

9
src/components/pages/about.tsx

@ -1,7 +1,8 @@
import React, { FC, useEffect } from 'react'
import { setTitle } from 'src/utils'
import PageHeader from 'src/components/page-header'
import Section from 'src/components/section'
import Title from 'src/components/title'
const About: FC = () => {
useEffect(() => {
@ -10,13 +11,13 @@ const About: FC = () => {
return (
<div>
<PageHeader title="About Flexor" />
<Section>
<Title>About Flexor</Title>
<div className="main-content">
<p>
Flexor is a website.
</p>
</div>
</Section>
</div>
)
}

13
src/components/pages/apps.tsx

@ -3,13 +3,16 @@ import { useSelector, useDispatch } from 'react-redux'
import { fetchApps } from 'src/actions/apps'
import { getApps } from 'src/selectors/apps'
import { useTheme } from 'src/hooks'
import { setTitle } from 'src/utils'
import { AppState, AppThunkDispatch, App } from 'src/types'
import PageHeader from 'src/components/page-header'
import Title from 'src/components/title'
import Section from 'src/components/section'
import AppListItem from 'src/components/app-list-item'
const Apps: FC = () => {
const theme = useTheme()
const apps = useSelector<AppState, App[]>(getApps)
const dispatch = useDispatch<AppThunkDispatch>()
@ -20,9 +23,13 @@ const Apps: FC = () => {
return (
<div>
<PageHeader title="Apps" />
<Section>
<Title>Apps</Title>
<div className="main-content">
<p style={{ color: theme.text }}>Use apps to post content to Flexor.</p>
</Section>
<div style={{ backgroundColor: theme.backgroundSecondary }}>
{apps.map(app => <AppListItem key={app.id} app={app} />)}
</div>
</div>

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

@ -1,7 +1,6 @@
import React, { FC, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Link, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons'
import { checkAppAvailability, createApp } from 'src/actions/apps'
@ -9,18 +8,21 @@ import { initForm, initField, setFieldNotification } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { getForm } from 'src/selectors/forms'
import { setTitle, valueFromForm } from 'src/utils'
import { AppState, Form, NotificationType, AppThunkDispatch } from 'src/types'
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'
import { AppState, Form, NotificationType, AppThunkDispatch, RequestKey } from 'src/types'
import Title from 'src/components/title'
import PrimaryButton from 'src/components/controls/primary-button'
import TextField from 'src/components/controls/text-field'
import TextareaField from 'src/components/controls/textarea-field'
import CheckboxField from 'src/components/controls/checkbox-field'
import ImageField from 'src/components/controls/image-field'
import CoverImageField from 'src/components/controls/cover-image-field'
import IconImageField from 'src/components/controls/icon-image-field'
import { getIsFetching } from 'src/selectors/requests'
const CreateApp: FC = () => {
const form = useSelector<AppState, Form>(getForm)
const fetching = useSelector<AppState, boolean>(state => getIsFetching(state, RequestKey.CreateApp))
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
@ -76,7 +78,11 @@ const CreateApp: FC = () => {
iconImageUrl,
}))
history.push(`/a/${id}`)
dispatch(showNotification(NotificationType.Success, 'App created successfully!'))
setTimeout(() => {
history.push(`/a/${id}`)
}, 1000)
}
useEffect(() => {
@ -95,45 +101,31 @@ const CreateApp: FC = () => {
return (
<div>
<PageHeader title="Create a new App" />
<div className="main-content">
<div className="centered-content-narrow">
<TextField name="name" label="Name" placeholder="App ID/Name" onBlur={e => checkAvailability(e.target.value)} />
<br />
<TextareaField name="about" label="About" placeholder="Description of this app" />
<br />
<TextField name="websiteUrl" label="Website" placeholder="Website URL (optional)" />
<br />
<TextField name="companyName" label="Company" placeholder="Your company or organization (optional)" />
<br />
<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" />
<br /><br />
<CheckboxField name="agree">
I agree to the Apps <Link to="/terms/apps">terms and conditions</Link>.
</CheckboxField>
<br /><br />
<button className="button is-success" onClick={() => handleCreate()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>
<span>Create</span>
</button>
</div>
<Title>Create a new App</Title>
<div>
<TextField name="name" label="Name" placeholder="App ID/Name" onBlur={e => checkAvailability(e.target.value)} />
<TextareaField name="about" label="About" placeholder="Description of this app" />
<TextField name="websiteUrl" label="Website" placeholder="Website URL (optional)" />
<TextField name="companyName" label="Company" placeholder="Your company or organization (optional)" />
<TextField name="version" label="Version" placeholder="Current Version of the app (ex: 0.0.1beta5)" />
<hr />
<ImageField name="image" />
<CoverImageField name="coverImage" />
<IconImageField name="iconImage" />
<hr />
<TextField name="composerUrl" label="Composer URL" placeholder="URL for the composer web page" />
<TextField name="rendererUrl" label="Renderer URL" placeholder="URL for the renderer template" />
<br />
<CheckboxField name="agree">
I agree to the Apps <Link to="/terms/apps">terms and conditions</Link>.
</CheckboxField>
<br /><br />
<PrimaryButton text="Create" icon={faCheckCircle} loading={fetching} onClick={() => handleCreate()} />
</div>
</div>
)

41
src/components/pages/developers.tsx

@ -1,7 +1,6 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useHistory } from 'react-router-dom'
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'
import { fetchCreatedApps } from 'src/actions/apps'
@ -9,10 +8,13 @@ import { getCreatedApps } from 'src/selectors/apps'
import { setTitle } from 'src/utils'
import { AppState, App, AppThunkDispatch } from 'src/types'
import PageHeader from 'src/components/page-header'
import Title from 'src/components/title'
import Subtitle from 'src/components/subtitle'
import PrimaryButton from 'src/components/controls/primary-button'
const Developers: FC = () => {
const apps = useSelector<AppState, App[]>(getCreatedApps)
const history = useHistory()
const dispatch = useDispatch<AppThunkDispatch>()
useEffect(() => {
@ -22,27 +24,18 @@ const Developers: FC = () => {
return (
<div>
<PageHeader title="Developers" />
<div className="main-content">
<div className="centered-content">
<h1 className="title has-text-success">Developer Documentation</h1>
<p>Flexor apps allow users to express themselves on the network.</p>
<br />
<p>Developer documentation coming soon.</p>
<hr />
<p>This is where you manage apps you create.</p>
<br />
<Link className="button is-primary" to="/developers/create">
<span className="icon is-small">
<FontAwesomeIcon icon={faPlusCircle} />
</span>
<span>Create a new App</span>
</Link>
</div>
</div>
<Title>Developers</Title>
<Subtitle>Developer Documentation</Subtitle>
<p>Flexor apps allow users to express themselves on the network.</p>
<br />
<p>Developer documentation coming soon.</p>
<hr />
<p>This is where you manage apps you create.</p>
<br />
<PrimaryButton text="Create a new App" icon={faPlusCircle} onClick={() => history.push('/developers/create')} />
</div>
)
}

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

@ -11,19 +11,22 @@ import { showNotification } from 'src/actions/notifications'
import { getAuthenticatedUserId } from 'src/selectors/authentication'
import { getEntity } from 'src/selectors/entities'
import { getForm } from 'src/selectors/forms'
import { getIsFetching } from 'src/selectors/requests'
import { useAuthenticationCheck, useDeepCompareEffect } from 'src/hooks'
import { setTitle, valueFromForm } from 'src/utils'
import { AppState, AppThunkDispatch, EntityType, App, Form, NotificationType } from 'src/types'
import { AppState, AppThunkDispatch, EntityType, App, Form, NotificationType, RequestKey } from 'src/types'
import PageHeader from 'src/components/page-header'
import Title from 'src/components/title'
import Loading from 'src/components/pages/loading'
import TextField from 'src/components/forms/text-field'
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'
import FieldLabel from 'src/components/controls/field-label'
import TextField from 'src/components/controls/text-field'
import TextareaField from 'src/components/controls/textarea-field'
import ImageField from 'src/components/controls/image-field'
import CoverImageField from 'src/components/controls/cover-image-field'
import IconImageField from 'src/components/controls/icon-image-field'
import PrimaryButton from 'src/components/controls/primary-button'
interface Params {
id: string
@ -33,6 +36,7 @@ const EditApp: FC = () => {
useAuthenticationCheck()
const { id } = useParams<Params>()
const fetching = useSelector<AppState, boolean>(state => getIsFetching(state, RequestKey.UpdateApp))
const app = useSelector<AppState, App | undefined>(state => getEntity<App>(state, EntityType.App, id))
const form = useSelector<AppState, Form>(getForm)
const selfId = useSelector<AppState, string | undefined>(getAuthenticatedUserId)
@ -110,8 +114,11 @@ const EditApp: FC = () => {
iconImageUrl,
}))
dispatch(showNotification(NotificationType.Success, 'Updated'))
history.push(`/a/${app.id}`)
dispatch(showNotification(NotificationType.Success, 'Updated!'))
setTimeout(() => {
history.push(`/a/${app.id}`)
}, 1000)
} catch (err) {
handleApiError(err, dispatch, history)
}
@ -121,76 +128,63 @@ const EditApp: FC = () => {
return (
<div>
<PageHeader title={app.name} />
<div className="main-content">
<div className="centered-content">
<div className="field">
<label className="label">Public Key</label>
<div className="control has-icons-left">
<input className="input" type="text" value={app.publicKey} readOnly />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faKey} />
</span>
</div>
</div>
<br />
<div className="field has-addons">
<p className="control">
<button className="button" onClick={() => setShowPrivateKey(!showPrivateKey)}>
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faShieldAlt} />
</span>
</button>
</p>
<p className="control is-expanded">
<input className="input" type="text" value={privateKeyDisplay} placeholder="Private Key" readOnly />
</p>
<Title>{app.name}</Title>
<div>
<div className="field">
<FieldLabel>Public Key</FieldLabel>
<div className="control-container">
<input className="input" type="text" value={app.publicKey} readOnly />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faKey} />
</span>
</div>
<br /><hr />
</div>
<br />
<div className="field">
<label className="label">ID</label>
<div className="control has-icons-left">
<input className="input" type="text" value={app.id} readOnly />
<div className="field">
<p className="control">
<button className="button" onClick={() => setShowPrivateKey(!showPrivateKey)}>
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faIdCard} />
<FontAwesomeIcon icon={faShieldAlt} />
</span>
</button>
</p>
<p className="control is-expanded">
<input className="input" type="text" value={privateKeyDisplay} placeholder="Private Key" readOnly />
</p>
</div>
<hr />
<div className="field">
<FieldLabel>ID</FieldLabel>
<div className="control-container">
<div className="icon">
<FontAwesomeIcon icon={faIdCard} />
</div>
<div className="control">
<input className="input" type="text" value={app.id} readOnly />
</div>
</div>
<br />
<TextField name="name" label="Name" placeholder="App Name" />
<br />
<TextareaField name="about" label="About" placeholder="Description of this app" />
<br />
<TextField name="websiteUrl" label="Website" placeholder="Website URL (optional)" />
<br />
<TextField name="companyName" label="Company" placeholder="Your company or organization (optional)" />
<br />
<TextField name="version" label="Version" placeholder="Current Version of the app (ex: 0.0.1beta5)" help={`Last Version: ${app.version}`} />
<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" />
<br /><br />
<button className="button is-primary" onClick={() => handleUpdate()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>
<span>Save</span>
</button>
</div>
<TextField name="name" label="Name" placeholder="App Name" />
<TextareaField name="about" label="About" placeholder="Description of this app" />
<TextField name="websiteUrl" label="Website" placeholder="Website URL (optional)" />
<TextField name="companyName" label="Company" placeholder="Your company or organization (optional)" />
<TextField name="version" label="Version" placeholder="Current Version of the app (ex: 0.0.1beta5)" help={`Last Version: ${app.version}`} />
<hr />
<ImageField name="image" />
<CoverImageField name="coverImage" />
<IconImageField name="iconImage" />
<hr />
<TextField name="composerUrl" label="Composer URL" placeholder="URL for the composer web page" />
<TextField name="rendererUrl" label="Renderer URL" placeholder="URL for the renderer template" />
<br /><br />
<PrimaryButton text="Save" icon={faCheckCircle} loading={fetching} onClick={() => handleUpdate()} />
</div>
</div>
)

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

@ -22,16 +22,18 @@ import {
Form,
} from 'src/types'
import PageHeader from 'src/components/page-header'
import Title from 'src/components/title'
import Subtitle from 'src/components/subtitle'
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'
import FieldLabel from 'src/components/controls/field-label'
import TextareaField from 'src/components/controls/textarea-field'
import ImageField from 'src/components/controls/image-field'
import CoverImageField from 'src/components/controls/cover-image-field'
import IconImageField from 'src/components/controls/icon-image-field'
interface Params {
id: string
@ -98,72 +100,71 @@ const GroupAdmin: FC = () => {
return (
<div>
<PageHeader title={group.name} subtitle="Administration" />
<div className="main-content">
<div className="centered-content">
<div className="tabs is-large">
<ul>
{tabs.map(t => (
<li key={t.id} className={tab === t.id ? 'is-active': ''}>
<Link to={`/c/${group.id}/admin/${t.id}`}>
{t.label}
</Link>
</li>
))}
</ul>
</div>
<div className="container">
{tab === '' &&
<div>
<div className="field">
<label className="label">ID</label>
<div className="control">
<input className="input" type="text" value={group.id} readOnly />
</div>
</div>
<br />
<Title>{group.name}</Title>
<Subtitle>Administration</Subtitle>
<div>
<div className="tabs is-large">
<ul>
{tabs.map(t => (
<li key={t.id} className={tab === t.id ? 'is-active': ''}>
<Link to={`/c/${group.id}/admin/${t.id}`}>
{t.label}
</Link>
</li>
))}
</ul>
</div>
<div className="field">
<label className="label">Name</label>
<div className="control">
<input className="input" type="text" value={group.name} readOnly />
</div>
<div className="container">
{tab === '' &&
<div>
<div className="field">
<FieldLabel>ID</FieldLabel>
<div className="control">
<input className="input" type="text" value={group.id} readOnly />
</div>
<br />
<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} />
</span>
<span>Save</span>
</button>
</div>
}
{tab === 'members' &&
<div>
<GroupInvitations group={id} />
<hr />
<br />
<h1 className="title is-size-4">Members</h1>
<MemberList group={id} />
<div className="field">
<FieldLabel>Name</FieldLabel>
<div className="control">
<input className="input" type="text" value={group.name} readOnly />
</div>
</div>
}
{tab === 'logs' && <GroupLogs group={id} />}
</div>
<br />
<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} />
</span>
<span>Save</span>
</button>
</div>
}
{tab === 'members' &&
<div>
<GroupInvitations group={id} />
<hr />
<Title>Members</Title>
<MemberList group={id} />
</div>
}
{tab === 'logs' && <GroupLogs group={id} />}
</div>
</div>
</div>

28
src/components/pages/groups.tsx

@ -1,7 +1,6 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useHistory } from 'react-router-dom'
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'
import { fetchGroups } from 'src/actions/groups'
@ -9,11 +8,13 @@ import { getGroups } from 'src/selectors/groups'
import { setTitle } from 'src/utils'
import { AppState, AppThunkDispatch, Group } from 'src/types'
import PageHeader from 'src/components/page-header'
import Title from 'src/components/title'
import GroupListItem from 'src/components/group-list-item'
import PrimaryButton from 'src/components/controls/primary-button'
const Groups: FC = () => {
const groups = useSelector<AppState, Group[]>(getGroups)
const history = useHistory()
const dispatch = useDispatch<AppThunkDispatch>()
useEffect(() => {
@ -23,20 +24,13 @@ const Groups: FC = () => {
return (
<div>
<PageHeader title="Communities" />
<div className="main-content">
{groups.map(group => <GroupListItem group={group} />)}
<hr />
<p className="has-text-centered">
<Link className="button is-primary" to="/register">
<span className="icon is-small">
<FontAwesomeIcon icon={faPlusCircle} />
</span>
<span>Create your own Community</span>
</Link>
</p>
<Title>Communities</Title>
{groups.map(group => <GroupListItem group={group} />)}
<hr />
<div style={{ textAlign: 'center' }}>
<PrimaryButton text="Create your own Community" icon={faPlusCircle} onClick={() => history.push('/register')} />
</div>
</div>
)

25
src/components/pages/home.tsx

@ -6,9 +6,11 @@ import { getAuthenticated } from 'src/selectors/authentication'
import { setTitle } from 'src/utils'
import { AppState, AppThunkDispatch } from 'src/types'
import PageHeader from 'src/components/page-header'
import Title from 'src/components/title'
import Composer from 'src/components/composer'
import Timeline from 'src/components/timeline'
import Section from 'src/components/section'
import Subtitle from 'src/components/subtitle'
const Home: FC = () => {
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
@ -24,16 +26,21 @@ const Home: FC = () => {
return (
<div>
<PageHeader title="Home" />
<Section>
<Title>Home</Title>
</Section>
<div className="main-content">
{authenticated &&
<div>
<Composer onPost={handlePost} />
<Timeline />
{authenticated &&
<div>
<Composer onPost={handlePost} />
<div style={{ padding: '0px 1rem' }}>
<Subtitle>Timeline</Subtitle>
</div>
}
</div>
<Timeline />
</div>
}
</div>
)
}

19
src/components/pages/loading.tsx

@ -1,15 +1,16 @@
import React, { FC } from 'react'
import PageHeader from 'src/components/page-header'
import { useTheme } from 'src/hooks'
import Title from 'src/components/title'
import Spinner from 'src/components/spinner'
const Loading: FC = () => (
<div>
<PageHeader title="Loading..." />
<div className="main-content">
<Spinner />
const Loading: FC = () => {
const theme = useTheme()
return (
<div>
<Title>Loading...</Title>
<Spinner color={theme.primary} />
</div>
</div>
)
)
}
export default Loading

42
src/components/pages/login.tsx

@ -1,9 +1,7 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useHistory } from 'react-router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faIdCard, faKey } from '@fortawesome/free-solid-svg-icons'
import classNames from 'classnames'
import { handleApiError } from 'src/api/errors'
import { authenticate } from 'src/actions/authentication'
@ -13,11 +11,12 @@ import { getFieldValue } from 'src/selectors/forms'
import { getIsFetching } from 'src/selectors/requests'
import { initForm, initField, setFieldNotification } from 'src/actions/forms'
import PageHeader from 'src/components/page-header'
import TextField from 'src/components/forms/text-field'
import PasswordField from 'src/components/forms/password-field'
import Title from 'src/components/title'
import TextField from 'src/components/controls/text-field'
import PasswordField from 'src/components/controls/password-field'
import PrimaryButton from 'src/components/controls/primary-button'
import { AppState, RequestKey, ClassDictionary, NotificationType } from 'src/types'
import { AppState, RequestKey, NotificationType } from 'src/types'
const Login: FC = () => {
const checked = useSelector<AppState, boolean>(getChecked)
@ -62,38 +61,15 @@ const Login: FC = () => {
handleApiError(err, dispatch, history)
}
}
const buttonClassDictionary: ClassDictionary = {
button: true,
'is-primary': true,
'is-loading': authenticating,
}
return (
<div>
<PageHeader title="Log In to Flexor" />
<div className="main-content">
<div className="centered-content">
<div className="centered-content-icon has-background-primary">
<span className="icon is-large has-text-white">
<FontAwesomeIcon icon={faKey} size="2x" />
</span>
</div>
<Title>Log In to Flexor</Title>
<TextField icon={faIdCard} name="name" label="Username" placeholder="Your Username/ID" />
<br />
<PasswordField placeholder="Your password" showStrength={false} />
<br />
<TextField icon={faIdCard} name="name" label="Username" placeholder="Your Username/ID" />
<PasswordField placeholder="Your password" showStrength={false} />
<button className={classNames(buttonClassDictionary)} onClick={() => handleAuthenticate()} disabled={authenticating}>
<span className="icon is-small">
<FontAwesomeIcon icon={faKey} />
</span>
<span>Log In</span>
</button>
</div>
</div>
<PrimaryButton text="Log In" icon={faKey} onClick={() => handleAuthenticate()} loading={authenticating} />
</div>
)
}

29
src/components/pages/register-group.tsx

@ -1,7 +1,6 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useParams, useHistory } from 'react-router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUserPlus } from '@fortawesome/free-solid-svg-icons'
import { handleApiError } from 'src/api/errors'
@ -17,7 +16,9 @@ import { useDeepCompareEffect } from 'src/hooks'
import { AppState, AppThunkDispatch, Group, EntityType, NotificationType, Form } from 'src/types'
import PageHeader from 'src/components/page-header'
import Title from 'src/components/title'
import Subtitle from 'src/components/subtitle'
import PrimaryButton from 'src/components/controls/primary-button'
import Loading from 'src/components/pages/loading'
import CreateUserForm from 'src/components/create-user-form'
@ -43,6 +44,7 @@ const RegisterGroup: FC = () => {
dispatch(initField('user-name', '', 'name'))
dispatch(initField('user-email', '', 'email'))
dispatch(initField('password', ''))
dispatch(initField('user-theme', '', 'theme'))
dispatch(initField('user-image', '', 'imageUrl'))
dispatch(initField('user-cover-image', '', 'coverImageUrl'))
dispatch(initField('user-agree', false))
@ -71,6 +73,7 @@ const RegisterGroup: FC = () => {
requiresApproval: valueFromForm<boolean>(form, 'user-requires-approval', false),
privacy: valueFromForm<string>(form, 'user-privacy', 'public'),
group: id,
theme: valueFromForm<string>(form, 'user-theme', ''),
}))
dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`))
@ -85,21 +88,13 @@ const RegisterGroup: FC = () => {
return (
<div>
<PageHeader title="Register" subtitle={group.name} />
<div className="main-content">
<div className="centered-content">
<CreateUserForm />
<br />
<button className="button is-primary" onClick={() => handleRegister()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faUserPlus} />
</span>
<span>Create Your Account</span>
</button>
</div>
</div>
<Title>Register</Title>
<Subtitle>{group.name}</Subtitle>
<CreateUserForm />
<br />
<PrimaryButton text="Create Your Account" icon={faUserPlus} onClick={() => handleRegister()} />
</div>
)
}

16
src/components/pages/register.tsx

@ -5,11 +5,11 @@ import { useHistory } from 'react-router'
import { handleApiError } from 'src/api/errors'
import { getForm } from 'src/selectors/forms'
import { getStep } from 'src/selectors/registration'
import { initForm, initField, setFieldValue } from 'src/actions/forms'
import { initForm, initField } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { createGroup, register } from 'src/actions/registration'
import PageHeader from 'src/components/page-header'
import Title from 'src/components/title'
import CreateGroupStep from 'src/components/create-group-step'
import CreateUserStep from 'src/components/create-user-step'
@ -41,7 +41,8 @@ const Register: FC = () => {
imageUrl: valueFromForm<string>(form, 'user-image', ''),
coverImageUrl: valueFromForm<string>(form, 'user-cover-image', ''),
requiresApproval: valueFromForm<boolean>(form, 'user-requires-approval', true),
privacy: valueFromForm<string>(form, 'user-privacy', 'open')
privacy: valueFromForm<string>(form, 'user-privacy', 'open'),
theme: valueFromForm<string>(form, 'user-theme', ''),
}))
await dispatch(createGroup({
@ -50,6 +51,7 @@ const Register: FC = () => {
imageUrl: valueFromForm<string>(form, 'group-image', ''),
coverImageUrl: valueFromForm<string>(form, 'group-cover-image', ''),
iconImageUrl: valueFromForm<string>(form, 'group-icon-image', ''),
theme: valueFromForm<string>(form, 'group-theme', ''),
}))
dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`))
@ -81,6 +83,7 @@ const Register: FC = () => {
dispatch(initField('group-image', '', 'imageUrl'))
dispatch(initField('group-cover-image', '', 'coverImageUrl'))
dispatch(initField('group-icon-image', '', 'iconImageUrl'))
dispatch(initField('group-theme', '', 'theme'))
dispatch(initField('group-agree', false))
dispatch(initField('user-id', '', 'id'))
dispatch(initField('user-name', '', 'name'))
@ -90,6 +93,7 @@ const Register: FC = () => {
dispatch(initField('user-cover-image', '', 'coverImageUrl'))
dispatch(initField('user-requires-approval', true, 'requiresApproval'))
dispatch(initField('user-privacy', 'public', 'privacy'))
dispatch(initField('user-theme', '', 'theme'))
dispatch(initField('user-agree', false))
}, [])
@ -99,11 +103,9 @@ const Register: FC = () => {
return (
<div>
<PageHeader title={title()} />
<Title>{title()}</Title>
<div className="main-content">
{component()}
</div>
{component()}
</div>
)
}

153
src/components/pages/self.tsx

@ -11,21 +11,28 @@ import { getForm } from 'src/selectors/forms'
import { handleApiError } from 'src/api/errors'
import { PRIVACY_OPTIONS } from 'src/constants'
import { useAuthenticationCheck, useDeepCompareEffect } from 'src/hooks'
import { useAuthenticationCheck, useDeepCompareEffect, useTheme } from 'src/hooks'
import { setTitle, valueFromForm } from 'src/utils'
import { AppState, User, Tab, Form } from 'src/types'
import PageHeader from 'src/components/page-header'
import { AppState, User, Form } from 'src/types'
import Title from 'src/components/title'
import Subtitle from 'src/components/subtitle'
import Section from 'src/components/section'
import HorizontalRule from 'src/components/horizontal-rule'
import PrimaryButton from 'src/components/controls/primary-button'
import SecondaryButton from 'src/components/controls/secondary-button'
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 ImageField from 'src/components/forms/image-field'
import CoverImageField from 'src/components/forms/cover-image-field'
import FieldLabel from 'src/components/controls/field-label'
import TextField from 'src/components/controls/text-field'
import TextareaField from 'src/components/controls/textarea-field'
import SelectField from 'src/components/controls/select-field'
import CheckboxField from 'src/components/controls/checkbox-field'
import ImageField from 'src/components/controls/image-field'
import CoverImageField from 'src/components/controls/cover-image-field'
import ThemeField from 'src/components/controls/theme-field'
const Self: FC = () => {
const theme = useTheme()
const dispatch = useDispatch()
const history = useHistory()
@ -47,6 +54,7 @@ const Self: FC = () => {
const privacy = valueFromForm<string>(form, 'privacy', 'public')
const imageUrl = valueFromForm<string>(form, 'image', '')
const coverImageUrl = valueFromForm<string>(form, 'coverImage', '')
const theme = valueFromForm<string>(form, 'theme', '')
try {
dispatch(updateSelf({
@ -56,6 +64,7 @@ const Self: FC = () => {
privacy,
imageUrl,
coverImageUrl,
theme,
}))
} catch (err) {
handleApiError(err, dispatch, history)
@ -73,6 +82,7 @@ const Self: FC = () => {
dispatch(initField('privacy', user.privacy))
dispatch(initField('image', user.imageUrl || ''))
dispatch(initField('coverImage', user.coverImageUrl || ''))
dispatch(initField('theme', user.theme))
}
}, [user])
@ -80,77 +90,68 @@ const Self: FC = () => {
return (
<div>
<PageHeader title={user.name || user.id} subtitle={`@${user.id}`} />
<div className="main-content">
<UserInfo user={user} />
<div className="centered-content">
<Link to={`/u/${user.id}`}>View Your Page</Link>
<br /><br />
<div className="field">
<label className="label">ID</label>
<div className="control has-icons-left">
<input className="input" type="text" value={user.id} readOnly />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faIdCard} />
</span>
<Section>
<Title>{user.name || user.id}</Title>
<Subtitle>@{user.id}</Subtitle>
<HorizontalRule />
<Link style={{ color: theme.primary }} to={`/u/${user.id}`}>Your Page</Link>
<div className="field">
<FieldLabel>ID</FieldLabel>
<div className="control-container">
<div className="icon" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<FontAwesomeIcon icon={faIdCard} />
</div>
</div>
<br />
<div className="field">
<label className="label">Email</label>
<div className="control has-icons-left">
<input className="input" type="email" value={user.email} readOnly />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faEnvelope} />
</span>
<div className="control">
<input
style={{ backgroundColor: theme.backgroundSecondary, borderColor: theme.secondary, color: theme.text }}
type="text"
value={user.id}
readOnly />
</div>
</div>
<br />
<TextField name="name" label="Name" placeholder="Your Display Name" />
<br />
<TextareaField name="about" label="About" placeholder="Your Bio" />
<br />
<SelectField name="privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<br />
<ImageField name="image" label="Avatar" />
<br />
<CoverImageField name="coverImage" />
<br />
<CheckboxField name="requiresApproval">
You must approve each Subscription request from other users.
</CheckboxField>
<br /><br />
<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>
</div>
<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 className="field">
<FieldLabel>Email</FieldLabel>
<div className="control-container">
<div className="icon" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<FontAwesomeIcon icon={faEnvelope} />
</div>
</nav>
<div className="control">
<input
style={{ backgroundColor: theme.backgroundSecondary, borderColor: theme.secondary, color: theme.text }}
type="email"
value={user.email}
readOnly />
</div>
</div>
</div>
</div>
<TextField name="name" label="Name" placeholder="Your Display Name" />
<TextareaField name="about" label="About" placeholder="Your Bio" />
<ThemeField name="color" label="Color" />
<SelectField name="privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<ImageField name="image" label="Avatar" />
<CoverImageField name="coverImage" />
<CheckboxField name="requiresApproval">
Approve each Subscription request from other users.
</CheckboxField>
<HorizontalRule />
<nav className="level">
<div>
<PrimaryButton text="Save" icon={faCheckCircle} onClick={() => handleUpdate()} />
</div>
<div>
<SecondaryButton text="Log Out" icon={faDoorOpen} onClick={() => handleLogout()} />
</div>
</nav>
</Section>
</div>
)
}

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

@ -1,28 +1,27 @@
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPlusSquare, faMinusSquare } from '@fortawesome/free-solid-svg-icons'
import moment from 'moment'
import { handleApiError } from 'src/api/errors'
import { fetchApp, installApp, uninstallApp } from 'src/actions/apps'
import { fetchInstallations } from 'src/actions/composer'
import { getAuthenticatedUserId } from 'src/selectors/authentication'
import { getInstallations } from 'src/selectors/composer'
import { getEntity } from 'src/selectors/entities'
import { getIsFetching } from 'src/selectors/requests'
import { useConfig } from 'src/hooks'
import { useConfig, useTheme } 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'
import { getInstallations } from 'src/selectors/composer'
import { AppState, AppThunkDispatch, EntityType, App, RequestKey, Installation, LevelItem } from 'src/types'
import PageHeader from 'src/components/page-header'
import Title from 'src/components/title'
import Section from 'src/components/section'
import HorizontalRule from 'src/components/horizontal-rule'
import Button from 'src/components/controls/button'
import Level from 'src/components/level'
import Loading from 'src/components/pages/loading'
import AppInfo from 'src/components/app-info'
import { ClassDictionary } from 'src/types'
interface Params {
id: string
@ -30,6 +29,7 @@ interface Params {
const ViewApp: FC = () => {
const { id } = useParams<Params>()
const theme = useTheme()
const app = useSelector<AppState, App | undefined>(state => getEntity<App>(state, EntityType.App, id))
const installations = useSelector<AppState, Installation[]>(getInstallations)
const selfId = useSelector<AppState, string | undefined>(getAuthenticatedUserId)
@ -57,13 +57,6 @@ const ViewApp: FC = () => {
const installed = !!installations.find(i => i.app.id === app.id)
const renderButton = () => {
const classes: ClassDictionary = {
'button': true,
'is-danger': installed,
'is-success': !installed,
'is-loading': fetching,
}
if (installed) {
const handleClick = async () => {
await dispatch(uninstallApp(id))
@ -71,12 +64,7 @@ const ViewApp: FC = () => {
}
return (
<button className={classNames(classes)} onClick={() => handleClick()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faMinusSquare} />
</span>
<span>Uninstall</span>
</button>
<Button text="Uninstall" icon={faMinusSquare} loading={fetching} onClick={() => handleClick()} color="#fff" backgroundColor={theme.red} />
)
} else {
const handleClick = async () => {
@ -85,51 +73,69 @@ const ViewApp: FC = () => {
}
return (
<button className={classNames(classes)} onClick={() => handleClick()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faPlusSquare} />
</span>
<span>Install</span>
</button>
<Button text="Install" icon={faPlusSquare} loading={fetching} onClick={() => handleClick()} color={theme.primaryAlternate} backgroundColor={theme.primary} />
)
}
}
const imageUrl = app.imageUrl ? urlForBlob(config, app.imageUrl) : undefined
const coverImageUrl = app.coverImageUrl ? urlForBlob(config, app.coverImageUrl) : undefined
const items: LevelItem[] = []
items.push({
label: 'Users',
content: app.users,
})
items.push({
label: 'Rating',
content: app.rating.toString(),
})
if (app.companyName) {
items.push({
label: 'Company',
content: app.companyName,
})
}
items.push({
label: 'Updated',
content: moment(app.updated).format('MMMM Do, YYYY'),
})
return (
<div>
<PageHeader title={app.name} />
<div className="main-content">
<AppInfo app={app} />
<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 has-text-primary">{app.name}</h1>
<br />
<p>{app.about}</p>
</div>
</div>
</article>
<br />
<Section>
{coverImageUrl &&
<div className="cover-image">
<img src={coverImageUrl} />
</div>
}
<div className="buttons">
{renderButton()}
{isCreator && <Link className="button is-primary" to={`/a/${id}/edit`}>View/Edit App</Link>}
<div className="header">
{imageUrl &&
<div className="image">
<img src={imageUrl} />
</div>
}
<div>
<Title>{app.name}</Title>
<p style={{ color: theme.text }}>{app.about}</p>
</div>
</div>
</div>
<Level items={items} />
<div style={{ textAlign: 'center', padding: '1rem' }}>
{renderButton()}
</div>
<HorizontalRule />
<div className="buttons">
{isCreator && <Link style={{ color: theme.secondary }} to={`/a/${id}/edit`}>View/Edit App</Link>}
</div>
</Section>
</div>
)
}

127
src/components/pages/view-group.tsx

@ -1,21 +1,25 @@
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 { useParams, useHistory } from 'react-router-dom'
import { faEdit, faUserCheck, faBan } from '@fortawesome/free-solid-svg-icons'
import moment from 'moment'
import { handleApiError } from 'src/api/errors'
import { fetchGroup } from 'src/actions/groups'
import { getAuthenticated } from 'src/selectors/authentication'
import { getEntity } from 'src/selectors/entities'
import { useDeepCompareEffect, useConfig } from 'src/hooks'
import { useDeepCompareEffect, useConfig, useTheme } from 'src/hooks'
import { setTitle, urlForBlob } from 'src/utils'
import { AppState, EntityType, Group, GroupMembershipType, AppThunkDispatch } from 'src/types'
import { AppState, EntityType, Group, GroupMembershipType, AppThunkDispatch, LevelItem } from 'src/types'
import PageHeader from 'src/components/page-header'
import GroupInfo from 'src/components/group-info'
import Title from 'src/components/title'
import Level from 'src/components/level'
import Section from 'src/components/section'
import PrimaryButton from 'src/components/controls/primary-button'
import Button from 'src/components/controls/button'
import Loading from 'src/components/pages/loading'
import { getAuthenticated } from 'src/selectors/authentication'
import HorizontalRule from 'src/components/horizontal-rule'
interface Params {
id: string
@ -23,6 +27,7 @@ interface Params {
const ViewGroup: FC = () => {
const { id } = useParams<Params>()
const theme = useTheme()
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const dispatch = useDispatch<AppThunkDispatch>()
@ -46,66 +51,68 @@ const ViewGroup: FC = () => {
const isAdmin = group.membership === GroupMembershipType.Admin
const isMember = !!group.membership
const imageUrl = group.imageUrl ? urlForBlob(config, group.imageUrl) : undefined
const coverImageUrl = group.coverImageUrl ? urlForBlob(config, group.coverImageUrl) : undefined
const items: LevelItem[] = []
items.push({
label: 'Members',
content: group.members,
})
items.push({
label: 'Posts',
content: group.posts,
})
items.push({
label: 'Awards',
content: group.awards,
})
items.push({
label: 'Points',
content: group.points,
})
items.push({
label: 'Created',
content: moment(group.updated).format('MMMM Do, YYYY'),
})
return (
<div>
<PageHeader title={group.name} />
<div className="main-content">
<GroupInfo group={group} />
<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 has-text-primary">{group.name}</h1>
<br />
<p>{group.about}</p>
</div>
<Section>
{coverImageUrl &&
<div className="cover-image">
<img src={coverImageUrl} />
</div>
}
<div className="header">
{imageUrl &&
<div className="image">
<img src={imageUrl} />
</div>
</article>
<br />
<div className="buttons">
{!authenticated &&
<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>
}
{!isMember &&
<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-primary">
<span className="icon is-small">
<FontAwesomeIcon icon={faEdit} />
</span>
<span>Edit {group.name}</span>
</Link>
}
}
<div>
<Title>{group.name}</Title>
<p style={{ color: theme.text }}>{group.about}</p>
</div>
</div>
</div>
<Level items={items} />
<HorizontalRule />
<div className="buttons">
{!authenticated &&
<PrimaryButton text="Create an Account" icon={faUserCheck} onClick={() => history.push(`/c/${group.id}/register`)} />
}
{!isMember &&
<Button text="Block" icon={faBan} onClick={() => history.push(`/c/${group.id}/register`)} color={theme.backgroundPrimary} backgroundColor={theme.red} />
}
{isAdmin &&
<PrimaryButton text={`Edit ${group.name}`} icon={faEdit} onClick={() => history.push(`/c/${group.id}/admin/`)} />
}
</div>
</Section>
</div>
)
}

48
src/components/pages/view-post.tsx

@ -13,7 +13,7 @@ import { getPostParents, getPostChildren } from 'src/selectors/posts'
import { setTitle } from 'src/utils'
import { AppState, AppThunkDispatch, EntityType, Post } from 'src/types'
import PageHeader from 'src/components/page-header'
import Title from 'src/components/title'
import Loading from 'src/components/pages/loading'
import PostComponent from 'src/components/post'
import PostList from 'src/components/post-list'
@ -53,36 +53,30 @@ const ViewPost: FC = () => {
return (
<div>
<PageHeader title="Post" />
<div className="main-content">
{parents.length > 0 &&
<div>
<PostList posts={parents} collapseText="Show Older Posts" />
<div className="has-text-centered">
<FontAwesomeIcon icon={faArrowsAltV} />
</div>
{parents.length > 0 &&
<div>
<PostList posts={parents} collapseText="Show Older Posts" />
<div style={{ textAlign: 'center' }}>
<FontAwesomeIcon icon={faArrowsAltV} />
</div>
}
</div>
}
<PostComponent post={post} />
<PostComponent post={post} />
{authenticated &&
<div>
<br />
<h1 className="title is-size-5 is-primary">Reply</h1>
<Composer parent={post} onPost={fetch} />
</div>
}
{authenticated &&
<div>
<Title>Reply</Title>
<Composer parent={post} onPost={fetch} />
</div>
}
{replies.length > 0 &&
<div>
<br />
<h1 className="title is-size-5">Replies</h1>
<PostList posts={replies} />
</div>
}
</div>
{replies.length > 0 &&
<div>
<Title>Replies</Title>
<PostList posts={replies} />
</div>
}
</div>
)
}

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

@ -1,8 +1,9 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Link, useParams, useHistory } from 'react-router-dom'
import { useParams, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUserPlus, faUserMinus, faUserClock, faBan } from '@fortawesome/free-solid-svg-icons'
import moment from 'moment'
import { handleApiError } from 'src/api/errors'
import { fetchUser, subscribe, unsubscribe } from 'src/actions/users'
@ -11,14 +12,17 @@ import { getEntity } from 'src/selectors/entities'
import { getAuthenticatedUser, getChecked } from 'src/selectors/authentication'
import { getUserPosts } from 'src/selectors/posts'
import { useDeepCompareEffect, useConfig } from 'src/hooks'
import { useDeepCompareEffect, useConfig, useTheme } from 'src/hooks'
import { setTitle, urlForBlob } from 'src/utils'
import { AppState, EntityType, User, Post, AppThunkDispatch } from 'src/types'
import { AppState, EntityType, User, Post, AppThunkDispatch, LevelItem } 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 Title from 'src/components/title'
import Subtitle from 'src/components/subtitle'
import Level from 'src/components/level'
import PostList from 'src/components/post-list'
import Section from 'src/components/section'
import HorizontalRule from 'src/components/horizontal-rule'
import Loading from 'src/components/pages/loading'
interface Params {
id: string
@ -26,6 +30,7 @@ interface Params {
const ViewUser: FC = () => {
const { id } = useParams<Params>()
const theme = useTheme()
const checked = useSelector<AppState, boolean>(getChecked)
const self = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const user = useSelector<AppState, User | undefined>(state => getEntity<User>(state, EntityType.User, id))
@ -56,87 +61,103 @@ const ViewUser: FC = () => {
const isSelf = self && self.id === user.id
const isGroup = self && self.group && user.group && self.group.id === user.group.id
const imageUrl = user.imageUrl ? urlForBlob(config, user.imageUrl) : undefined
const coverImageUrl = user.coverImageUrl ? urlForBlob(config, user.coverImageUrl) : undefined
const subscription = self && user.subscriptions ? user.subscriptions.find(subscription => subscription.from === self.id && subscription.to === user.id) : undefined
const subscribed = subscription && !subscription.pending
const subscriptionPending = subscription && subscription.pending
const items: LevelItem[] = []
items.push({
label: 'Posts',
content: user.posts,
})
items.push({
label: 'Awards',
content: user.awards,
})
items.push({
label: 'Points',
content: user.points,
})
items.push({
label: 'Joined',
content: moment(user.created).format('MMMM Do, YYYY'),
})
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>
<Section>
{coverImageUrl &&
<div className="cover-image">
<img src={coverImageUrl} />
</div>
}
<div className="header">
{imageUrl &&
<div className="image">
<img src={imageUrl} style={{ width: 128 }} />
</div>
</article>
<br />
<div className="buttons">
{subscribed &&
<button className="button is-danger" onClick={() => dispatch(unsubscribe(user.id))}>
<span className="icon is-small">
<FontAwesomeIcon icon={faUserMinus} />
</span>
<span>Unsusbcribe</span>
</button>
}
{subscriptionPending &&
<button className="button is-warning">
<span className="icon is-small">
<FontAwesomeIcon icon={faUserClock} />
</span>
<span>Pending</span>
</button>
}
{self && !isSelf && !subscribed && !subscriptionPending &&
<button className="button is-success" onClick={() => dispatch(subscribe(user.id))}>
<span className="icon is-small">
<FontAwesomeIcon icon={faUserPlus} />
</span>
<span>Subscribe</span>
</button>
}
{!isSelf &&
<button className="button is-danger">
<span className="icon is-small">
<FontAwesomeIcon icon={faBan} />
</span>
<span>Block</span>
</button>
}
{user.group && !isGroup &&
<button className="button is-danger">
<span className="icon is-small">
<FontAwesomeIcon icon={faBan} />
</span>
<span>Block Community: {user.group.name}</span>
</button>
}
}
<div>
<Title>{user.name}</Title>
<p style={{ color: theme.text }}>{user.about}</p>
</div>
</div>
<Level items={items} />
<HorizontalRule />
<div className="buttons">
{subscribed &&
<button className="button is-danger" onClick={() => dispatch(unsubscribe(user.id))}>
<span className="icon is-small">
<FontAwesomeIcon icon={faUserMinus} />
</span>
<span>Unsusbcribe</span>
</button>
}
{subscriptionPending &&
<button className="button is-warning">
<span className="icon is-small">
<FontAwesomeIcon icon={faUserClock} />
</span>
<span>Pending</span>
</button>
}
{self && !isSelf && !subscribed && !subscriptionPending &&
<button className="button is-success" onClick={() => dispatch(subscribe(user.id))}>
<span className="icon is-small">
<FontAwesomeIcon icon={faUserPlus} />
</span>
<span>Subscribe</span>
</button>
}
{!isSelf &&
<button className="button is-danger">
<span className="icon is-small">
<FontAwesomeIcon icon={faBan} />
</span>
<span>Block</span>
</button>
}
{user.group && !isGroup &&
<button className="button is-danger">
<span className="icon is-small">
<FontAwesomeIcon icon={faBan} />
</span>
<span>Block Community: {user.group.name}</span>
</button>
}
</div>
</Section>
<div style={{ padding: '0px 1rem' }}>
<Subtitle>Posts</Subtitle>
</div>
<PostList posts={posts} />

8
src/components/post-list.tsx

@ -1,5 +1,6 @@
import React, { FC, useState } from 'react'
import classNames from 'classnames'
import { useTheme } from 'src/hooks'
import { classNames } from 'src/utils'
import { Post, ClassDictionary } from 'src/types'
import PostComponent from 'src/components/post'
@ -10,15 +11,16 @@ interface Props {
}
const PostList: FC<Props> = ({ posts, collapseText }) => {
const theme = useTheme()
const [isCollapsed, setIsCollapsed] = useState(!!collapseText)
const classes: ClassDictionary = {
const style: ClassDictionary = {
'post-list': true,
'post-list-collapsed': isCollapsed,
}
return (
<div className={classNames(classes)}>
<div className={classNames(style)} style={{ backgroundColor: theme.backgroundSecondary }}>
{isCollapsed &&
<button className="button is-primary is-fullwidth" onClick={() => setIsCollapsed(false)}>{collapseText}</button>
}

24
src/components/post.tsx

@ -4,17 +4,19 @@ import { Link } from 'react-router-dom'
import moment from 'moment'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClock, faReplyAll, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'
import { useTheme } from 'src/hooks'
import { setEntities } from 'src/actions/entities'
import { normalize } from 'src/utils/normalization'
import { Post, EntityType } from 'src/types'
import User from 'src/components/user'
import { normalize } from 'src/utils/normalization'
interface Props {
post: Post
}
const PostComponent: FC<Props> = ({ post }) => {
const theme = useTheme()
const dispatch = useDispatch()
const showCover = !!post.cover && !post.revealed
@ -28,14 +30,14 @@ const PostComponent: FC<Props> = ({ post }) => {
}
return (
<div className="post">
<div className="post" style={{ backgroundColor: theme.backgroundPrimary, borderColor: theme.backgroundSecondary, color: theme.text }}>
{showCover &&
<div className="cover" onClick={() => handleShowPost()}>{post.cover}</div>
<div className="cover" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }} onClick={() => handleShowPost()}>{post.cover}</div>
}
{!showCover &&
<div>
{post.text && <p className="is-size-5">{post.text}</p>}
<div className="post-content">
{post.text && <p>{post.text}</p>}
{post.attachments && post.attachments.length > 0 &&
<div className="attachments">
@ -50,22 +52,22 @@ const PostComponent: FC<Props> = ({ post }) => {
</div>
}
<div className="post-info">
<div className="post-info" style={{ borderColor: theme.backgroundSecondary }}>
<div>
<User user={post.user} />
</div>
{!!post.cover && post.cover.length > 0 &&
<div>
<span className="icon">
<span className="icon" style={{ color: theme.red }}>
<FontAwesomeIcon icon={faExclamationCircle} />
</span>
</div>
}
<div>
<Link to={`/p/${post.id}`}>
<span className="icon">
<Link to={`/p/${post.id}`} style={{ color: theme.primary }}>
<span className="icon" style={{ color: theme.secondary }}>
<FontAwesomeIcon icon={faReplyAll} />
</span>
{post.replies}
@ -73,10 +75,10 @@ const PostComponent: FC<Props> = ({ post }) => {
</div>
<div>
<span className="icon">
<span className="icon" style={{ color: theme.secondary }}>
<FontAwesomeIcon icon={faClock} />
</span>
<Link to={`/p/${post.id}`} className="has-text-primary">
<Link to={`/p/${post.id}`} style={{ color: theme.primary }}>
{moment(post.created).format('MMMM Do, h:mm A')}
</Link>
</div>

16
src/components/search.tsx

@ -0,0 +1,16 @@
import React, { FC } from 'react'
import { useTheme } from 'src/hooks'
const App: FC = () => {
const theme = useTheme()
return (
<div className="search">
<input
style={{ backgroundColor: theme.backgroundPrimary, borderColor: theme.primary, color: theme.text }}
type="text"
placeholder="Search" />
</div>
)
}
export default App

13
src/components/section.tsx

@ -0,0 +1,13 @@
import React, { FC } from 'react'
import { useTheme } from 'src/hooks'
const Section: FC = ({ children }) => {
const theme = useTheme()
return (
<section style={{ backgroundColor: theme.backgroundPrimary }}>
{children}
</section>
)
}
export default Section

92
src/components/self-info.tsx

@ -1,23 +1,35 @@
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 { useConfig, useTheme } from 'src/hooks'
import { urlForBlob } from 'src/utils'
import { AppState, User, Config } from 'src/types'
import { AppState, User } from 'src/types'
const SelfInfo: FC = () => {
const theme = useTheme()
const config = useConfig()
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const config = useSelector<AppState, Config>(getConfig)
const imageUrl = user && user.imageUrl ? urlForBlob(config, user.imageUrl) : undefined
if (!user) {
return (
<div className="user-info unauthenticated" style={{ backgroundColor: theme.primary }}>
<Link to="/login" style={{ color: theme.primaryAlternate, fontSize: '1.1rem' }}>Log In to Flexor</Link>
<p className="divider">or</p>
<Link to="/communities" style={{ color: theme.backgroundSecondary, fontSize: '0.9rem' }}>Create an Account</Link>
</div>
)
}
const group = user.group
const imageUrl = user.imageUrl ? urlForBlob(config, user.imageUrl) : undefined
const groupImageUrl = group && group.iconImageUrl ? urlForBlob(config, group.iconImageUrl) : undefined
const name = (user: User) => {
const name = () => {
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 to="/self" style={{ color: theme.primaryAlternate }}>
<span style={{ fontSize: '1.1rem' }}>{user.name}</span> <span style={{ fontSize: '1rem', fontWeight: 'bold' }}>@{user.id}</span>
</Link>
)
}
@ -25,54 +37,30 @@ const SelfInfo: FC = () => {
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">
<div className="user-info" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
{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="image">
<img src={imageUrl} style={{ width: 32 }} />
</div>
}
<div>
{name()}
<br />
{group &&
<div className="group-info">
{groupImageUrl &&
<div className="image">
<img src={groupImageUrl} style={{ width: 16 }} />
</div>
}
<div>
<Link to={`/c/${group.id}`} style={{ color: theme.backgroundSecondary }}>{group.name}</Link>
</div>
</div>
}
</div>
</article>
</div>
)
}

24
src/components/spinner.tsx

@ -1,16 +1,20 @@
import React, { FC } from 'react'
const Spinner: FC = () => (
interface Props {
color: string
}
const Spinner: FC<Props> = ({ color }) => (
<div className="sk-cube-grid">
<div className="sk-cube sk-cube1"></div>
<div className="sk-cube sk-cube2"></div>
<div className="sk-cube sk-cube3"></div>
<div className="sk-cube sk-cube4"></div>
<div className="sk-cube sk-cube5"></div>
<div className="sk-cube sk-cube6"></div>
<div className="sk-cube sk-cube7"></div>
<div className="sk-cube sk-cube8"></div>
<div className="sk-cube sk-cube9"></div>
<div className="sk-cube sk-cube1" style={{ backgroundColor: color }}></div>
<div className="sk-cube sk-cube2" style={{ backgroundColor: color }}></div>
<div className="sk-cube sk-cube3" style={{ backgroundColor: color }}></div>
<div className="sk-cube sk-cube4" style={{ backgroundColor: color }}></div>
<div className="sk-cube sk-cube5" style={{ backgroundColor: color }}></div>
<div className="sk-cube sk-cube6" style={{ backgroundColor: color }}></div>
<div className="sk-cube sk-cube7" style={{ backgroundColor: color }}></div>
<div className="sk-cube sk-cube8" style={{ backgroundColor: color }}></div>
<div className="sk-cube sk-cube9" style={{ backgroundColor: color }}></div>
</div>
)

9
src/components/subtitle.tsx

@ -0,0 +1,9 @@
import React, { FC } from 'react'
import { useTheme } from 'src/hooks'
const Subtitle: FC = ({ children }) => {
const theme = useTheme()
return <h2 className="subtitle" style={{ color: theme.secondary }}>{children}</h2>
}
export default Subtitle

6
src/components/timeline.tsx

@ -29,11 +29,7 @@ const Timeline: FC = () => {
if (authenticated) init()
}, [authenticated])
return (
<div>
<PostList posts={posts} />
</div>
)
return <PostList posts={posts} />
}
export default Timeline

9
src/components/title.tsx

@ -0,0 +1,9 @@
import React, { FC } from 'react'
import { useTheme } from 'src/hooks'
const Title: FC = ({ children }) => {
const theme = useTheme()
return <h1 style={{ color: theme.primary }}>{children}</h1>
}
export default Title

42
src/components/user-info.tsx

@ -1,42 +0,0 @@
import React, { FC } from 'react'
import moment from 'moment'
import { User } from 'src/types'
interface Props {
user: User
}
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>
<div className="level-item has-text-centered">
<div>
<p className="heading has-text-success">Awards</p>
<p className="title">{user.awards}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">Points</p>
<p className="title">{user.points}</p>
</div>
</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>
</nav>
)
export default UserInfo

32
src/components/user.tsx

@ -1,39 +1,39 @@
import React, { FC } from 'react'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { getConfig } from 'src/selectors'
import { useTheme, useConfig } from 'src/hooks'
import { urlForBlob } from 'src/utils'
import { AppState, User, Config } from 'src/types'
import { User } from 'src/types'
interface Props {
user: User
}
const UserComponent: FC<Props> = ({ user }) => {
const config = useSelector<AppState, Config>(getConfig)
const theme = useTheme()
const config = useConfig()
const imageUrl = user && user.imageUrl ? urlForBlob(config, user.imageUrl) : undefined
const groupImageUrl = user.group && user.group.iconImageUrl ? urlForBlob(config, user.group.iconImageUrl) : undefined
return (
<div className="user">
{imageUrl &&
<div className="avatar">
<div className="image">
<img src={imageUrl} style={{ width: 32 }} />
</div>
}
<div>
<Link to={`/u/${user.id}`}>
<span className="is-size-5">{user.name}</span> <span className="is-size-6 has-text-weight-bold">@{user.id}</span>
<Link style={{ color: theme.primary }} to={`/u/${user.id}`}>
<span style={{ fontSize: '0.9rem' }}>{user.name}</span> <span style={{ fontSize: '0.8rem', fontWeight: 'bold' }}>@{user.id}</span>
</Link>
<br />
{groupImageUrl &&
<figure className="image is-16x16 is-inline">
<img src={groupImageUrl} style={{ width: 16 }} />
</figure>
}
&nbsp;&nbsp;
{user.group && <Link className="is-size-6 has-text-success" to={`/c/${user.group.id}`}>{user.group.name}</Link>}
<div className="group">
{groupImageUrl &&
<div className="image">
<img src={groupImageUrl} style={{ width: 16 }} />
</div>
}
{user.group && <Link style={{ color: theme.secondary }} to={`/c/${user.group.id}`}>{user.group.name}</Link>}
</div>
</div>
</div>
)

5
src/hooks/index.ts

@ -4,8 +4,9 @@ import { useHistory } from 'react-router-dom'
import isEqual from 'lodash/isEqual'
import { getAuthenticated, getChecked } from 'src/selectors/authentication'
import { getTheme } from 'src/selectors/theme'
import { getConfig } from 'src/selectors'
import { AppState, Config } from 'src/types'
import { AppState, Theme, Config } from 'src/types'
export const useAuthenticationCheck = () => {
const checked = useSelector<AppState, boolean>(getChecked)
@ -32,3 +33,5 @@ const useDeepCompareMemoize = (value: any) => {
export const useDeepCompareEffect = (callback: EffectCallback, deps?: readonly any[] | undefined) => {
useEffect(callback, useDeepCompareMemoize(deps))
}
export const useTheme = () => useSelector<AppState, Theme>(getTheme)

22
src/reducers/menu.ts

@ -1,22 +0,0 @@
import { Reducer } from 'redux'
import { MenuActions } from '../actions/menu'
import { MenuState } from '../types'
const initialState: MenuState = {
collapsed: false,
}
const reducer: Reducer<MenuState, MenuActions> = (state = initialState, action) => {
switch (action.type) {
case 'MENU_SET_COLLAPSED':
return {
...state,
collapsed: action.payload,
}
default:
return state
}
}
export default reducer

30
src/reducers/theme.ts

@ -0,0 +1,30 @@
import { Reducer } from 'redux'
import { ThemeActions } from '../actions/theme'
import { ThemeState, ColorScheme } from '../types'
const initialState: ThemeState = {
scheme: ColorScheme.Light,
name: 'blue',
}
const reducer: Reducer<ThemeState, ThemeActions> = (state = initialState, action) => {
switch (action.type) {
case 'THEME_SET_THEME': {
return {
...state,
name: action.payload,
}
}
case 'THEME_SET_COLOR_SCHEME': {
return {
...state,
scheme: action.payload,
}
}
default:
return state
}
}
export default reducer

3
src/selectors/menu.ts

@ -1,3 +0,0 @@
import { AppState } from '../types'
export const getCollapsed = (state: AppState) => state.menu.collapsed

6
src/selectors/theme.ts

@ -0,0 +1,6 @@
import themes from 'src/themes'
import { AppState } from 'src/types'
export const getTheme = (state: AppState) => themes[state.theme.name][state.theme.scheme]
export const getThemeName = (state: AppState) => state.theme.name
export const getColorScheme = (state: AppState) => state.theme.scheme

4
src/store/index.ts

@ -7,10 +7,10 @@ import config from '../reducers/config'
import entities from '../reducers/entities'
import forms from '../reducers/forms'
import lists from '../reducers/lists'
import menu from '../reducers/menu'
import notifications from '../reducers/notifications'
import registration from '../reducers/registration'
import requests from '../reducers/requests'
import theme from '../reducers/theme'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
@ -23,10 +23,10 @@ const store = createStore(
entities,
forms,
lists,
menu,
notifications,
registration,
requests,
theme,
}),
applyMiddleware(thunk, logger)
)

367
src/styles/app.css

@ -0,0 +1,367 @@
@charset "utf-8";
@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro&display=swap');
:root {
--default-border: 1px solid;
--default-font: 'Source Sans Pro', sans-serif;
--input-padding: 0.5rem;
--content-width: 600px;
--menu-width: 270px;
}
html {
font-family: var(--default-font);
font-size: 18px;
}
body, div, h1, h2, input, select, textarea, label, button, p.help, section, div.icon {
transition: color 1s;
}
input, select, textarea, button, div, div.content, div.menu, section, div.icon {
transition: background-color 1s, border-color 1s;
}
body {
font-family: var(--default-font);
margin: 0px;
padding: 0px;
}
input, textarea, select {
border: var(--default-border);
box-sizing: border-box;
font-family: var(--default-font);
font-size: 1rem;
padding: var(--input-padding);
width: 100%;
}
input[type="file"] {
display: none;
}
h1 {
font-size: 2rem;
margin: 0.5rem 0;
}
h2 {
font-size: 1.2rem;
}
h1 + h2 {
margin-top: -0.5rem;
}
a {
text-decoration: none;
}
hr {
margin: 1rem 0px;
}
main {
bottom: 0;
display: flex;
justify-content: center;
left: 0;
padding: 0px;
position: absolute;
right: 0;
top: 0;
}
section {
padding: 1rem;
}
iframe {
border: none;
overflow: hidden;
width: 100%;
}
button, label.file-input {
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 700;
padding: 0.5rem 1rem;
min-width: 100px;
}
div.logo-container {
padding-top: 1rem;
width: 60px;
}
div.logo {
--size: 40px;
--padding-top: 7px;
border-radius: 90px;
font-size: 20px;
font-weight: bold;
height: calc(var(--size) - var(--padding-top));
margin: 10px;
padding-top: var(--padding-top);
position: fixed;
text-align: center;
width: var(--size);
}
div.content {
border-left: var(--default-border);
border-right: var(--default-border);
width: var(--content-width);
}
div.menu-container {
margin: 0px;
padding: 0px;
width: var(--menu-width);
}
div.menu {
bottom: 0;
display: flex;
flex-direction: column;
margin: 0px;
position: fixed;
top: 0;
width: var(--menu-width);
}
div.menu > nav {
flex-grow: 1;
padding: 1rem;
}
div.menu > nav > div {
margin-bottom: 0.9rem;
}
.icon {
display: inline-block;
margin-right: 5px;
}
footer {
font-size: 0.8rem;
padding: 0.9rem;
text-align: center;
}
div.field {
margin: 1rem 0px;
}
div.field div.label {
font-weight: 700;
}
div.control-container {
display: flex;
padding: 0.5rem 0px;
}
div.control-container > div.icon {
margin: 0px;
padding: var(--input-padding);
}
div.control {
flex-grow: 1;
}
p.help {
font-size: 0.8rem;
}
div.search {
padding: 10px;
text-align: center;
}
div.notification-container {
bottom: 10px;
position: fixed;
left: 10px;
width: 40%;
}
nav.level {
display: flex;
justify-content: space-between;
}
nav.level > div {
text-align: center;
}
nav.level p.label {
font-size: 0.9rem;
font-weight: bold;
}
nav.level p.content {
font-size: 1.1rem;
}
p.label + p.content {
margin-top: -10px;
}
div.member {
border: var(--default-border);
margin-right: 10px;
min-width: 150px;
padding: 1rem;
}
div.composer-container {
border-top: var(--default-border);
border-bottom: var(--default-border);
}
div.composer-empty, div.composer-empty {
font-size: 0.8rem;
text-align: center;
padding: 1.5rem;
}
div.installations {
display: flex;
padding: 0.5rem;
}
div.installations > div {
border-right: var(--default-border);
padding: 0.5rem;
text-align: center;
}
div.installations > div > p {
font-size: 0.6rem;
font-weight: bold;
margin: 0px;
padding: 0px;
}
div.user-info, div.group-info {
align-items: center;
display: flex;
}
div.user-info div.image, div.group-info, div.image {
margin-right: 10px;
}
div.user-info {
display: flex;
padding: 1.5rem 1rem;
}
div.group-info {
display: flex;
}
div.post {
border-top: var(--default-border);
border-bottom: var(--default-border);
margin: 1rem 0px;
}
div.post div.post-content {
padding: 1rem;
}
div.post div.cover {
cursor: pointer;
font-weight: bold;
padding: 2rem;
text-align: center;
}
div.post-info {
border-top: var(--default-border);
display: flex;
font-size: 0.8rem;
justify-content: space-between;
}
div.post-info > div {
padding: 0.8rem;
}
div.user {
align-items: flex-start;
display: flex;
}
div.user div.group div.image {
display: inline-block;
margin-right: 5px;
vertical-align: sub;
}
div.theme-picker {
display: flex;
flex-wrap: wrap;
margin: 0.5rem 0px;
}
div.theme-picker > div {
--size: 25px;
border: 2px solid;
height: var(--size);
margin-bottom: 10px;
margin-right: 10px;
width: var(--size);
}
div.theme-picker + div {
font-size: 0.8rem;
font-weight: 700;
margin-top: -10px;
}
div.cover-image {
width: var(--content-width);
}
div.cover-image img {
width: var(--content-width);
}
div.header {
display: flex;
}
div.cover-image + div.header {
margin-top: -20px;
}
div.header img {
width: 128px;
}
div.app-list-item {
display: flex;
margin: 1rem 0px;
padding: 1rem;
}
div.app-list-item p {
font-size: 0.8rem;
padding: 0.5rem;
padding-top: 0px;
}
div.app-list-item img {
width: 64px;
}

207
src/styles/app.scss

@ -1,207 +0,0 @@
@charset "utf-8";
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700&display=swap');
// Colors
$orange: hsl(14, 100%, 53%);
$yellow: hsl(48, 100%, 67%);
$green: hsl(141, 65%, 31%);
$turquoise: hsl(171, 100%, 41%);
$cyan: hsl(204, 86%, 53%);
$blue: hsl(217, 72%, 30%);
$purple: hsl(271, 63%, 32%);
$red: hsl(348, 71%, 42%);
$grey: hsl(0, 0%, 48%);
$grey-light: hsl(0, 0%, 71%);
$grey-lighter: hsl(0, 0%, 86%);
$white-ter: hsl(0, 0%, 96%);
$white-bis: hsl(0, 0%, 98%);
$family-sans-serif: "Open Sans", sans-serif;
$primary: $blue;
$body-background-color: $white-ter;
$body-size: 14px;
@import "../../node_modules/bulma/sass/utilities/_all.sass";
@import "../../node_modules/bulma/sass/base/_all.sass";
@import "../../node_modules/bulma/sass/form/_all.sass";
@import "../../node_modules/bulma/sass/grid/columns.sass";
@import "../../node_modules/bulma/sass/elements/button.sass";
@import "../../node_modules/bulma/sass/elements/icon.sass";
@import "../../node_modules/bulma/sass/elements/notification.sass";
@import "../../node_modules/bulma/sass/elements/other.sass";
@import "../../node_modules/bulma/sass/elements/table.sass";
@import "../../node_modules/bulma/sass/elements/tag.sass";
@import "../../node_modules/bulma/sass/elements/title.sass";
@import "../../node_modules/bulma/sass/elements/progress.sass";
@import "../../node_modules/bulma/sass/layout/hero.sass";
@import "../../node_modules/bulma/sass/components/level.sass";
@import "../../node_modules/bulma/sass/components/media.sass";
@import "../../node_modules/bulma/sass/components/tabs.sass";
div#main-menu {
background: linear-gradient(135deg, $primary, darken($primary, 20%));
bottom: 0;
display: flex;
flex-direction: column;
position: fixed;
right: 0;
top: 0;
}
div#header, div#navigation {
padding: $size-normal;
}
div.main-content {
padding: $size-normal;
}
div.centered-content {
background-color: $white;
border-radius: $radius;
margin: 1rem auto;
padding: 2rem;
width: 95%;
div.centered-content-icon {
border-radius: 100px;
margin: auto;
margin-top: -20px;
text-align: center;
width: 3rem;
}
}
div.centered-content-narrow {
@extend div.centered-content;
width: 75%;
}
div#navigation {
flex-grow: 1;
div {
margin: 1rem 0px;
}
}
footer {
padding: $size-normal;
}
div#notification-container {
bottom: 10px;
position: fixed;
left: 10px;
width: 40%;
}
div.group-list-item, div.app-list-item {
background-color: $white;
border-radius: 15px;
margin: 10px 0px;
padding: 20px;
}
div.member {
border: solid 1px $grey-lighter;
margin-right: 10px;
min-width: 150px;
padding: 1rem;
}
div.invitation-options {
display: flex;
}
div.invitation-options > div {
margin-right: 20px;
}
article#user-info {
padding: 20px;
}
div.composer-container {
background-color: white;
border: solid 1px $primary;
div.composer {
color: $primary;
font-weight: bold;
text-align: center;
}
div.composer-empty {
padding: 3rem;
}
div.installations {
background-color: $white-bis;
div {
border: solid 2px $white-bis;
cursor: pointer;
margin: 10px;
padding: 5px 15px;
text-align: center;
}
div.selected {
border: solid 2px $green;
}
}
}
div.user {
display: flex;
div.avatar {
margin-top: 6px;
margin-right: 10px;
}
}
div.post-list {
margin: 10px;
}
div.post {
background-color: white;
margin: 10px 0;
}
div.post p {
padding: 20px;
}
div.post > div.cover {
background: linear-gradient(135deg, $white-ter, $grey-light);
cursor: pointer;
font-size: 1.1rem;
font-weight: bold;
padding: 30px;
text-align: center;
}
div.post-info {
border-top: solid 1px $grey-lighter;
display: flex;
justify-content: space-between;
padding: 0 20px;
}
div.post-info > div {
padding: 10px;
}
div.attachment {
padding: 10px;
p.caption {
color: $grey;
font-size: 1rem;
margin-top: -15px;
}
}

47
src/styles/spinner.css

@ -0,0 +1,47 @@
.sk-cube-grid {
width: 30px;
height: 30px;
margin: 10px auto;
}
.sk-cube-grid .sk-cube {
width: 33%;
height: 33%;
float: left;
animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
}
.sk-cube-grid .sk-cube1 {
animation-delay: 0.2s; }
.sk-cube-grid .sk-cube2 {
animation-delay: 0.3s; }
.sk-cube-grid .sk-cube3 {
animation-delay: 0.4s; }
.sk-cube-grid .sk-cube4 {
animation-delay: 0.1s; }
.sk-cube-grid .sk-cube5 {
animation-delay: 0.2s; }
.sk-cube-grid .sk-cube6 {
animation-delay: 0.3s; }
.sk-cube-grid .sk-cube7 {
animation-delay: 0s; }
.sk-cube-grid .sk-cube8 {
animation-delay: 0.1s; }
.sk-cube-grid .sk-cube9 {
animation-delay: 0.2s; }
@-webkit-keyframes sk-cubeGridScaleDelay {
0%, 70%, 100% {
transform: scale3D(1, 1, 1);
} 35% {
transform: scale3D(0, 0, 1);
}
}
@keyframes sk-cubeGridScaleDelay {
0%, 70%, 100% {
transform: scale3D(1, 1, 1);
} 35% {
transform: scale3D(0, 0, 1);
}
}

62
src/styles/spinner.scss

@ -1,62 +0,0 @@
.sk-cube-grid {
width: 30px;
height: 30px;
margin: 10px auto;
}
.sk-cube-grid .sk-cube {
width: 33%;
height: 33%;
background-color: #000;
float: left;
-webkit-animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
}
.sk-cube-grid .sk-cube1 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s; }
.sk-cube-grid .sk-cube2 {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s; }
.sk-cube-grid .sk-cube3 {
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s; }
.sk-cube-grid .sk-cube4 {
-webkit-animation-delay: 0.1s;
animation-delay: 0.1s; }
.sk-cube-grid .sk-cube5 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s; }
.sk-cube-grid .sk-cube6 {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s; }
.sk-cube-grid .sk-cube7 {
-webkit-animation-delay: 0s;
animation-delay: 0s; }
.sk-cube-grid .sk-cube8 {
-webkit-animation-delay: 0.1s;
animation-delay: 0.1s; }
.sk-cube-grid .sk-cube9 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s; }
@-webkit-keyframes sk-cubeGridScaleDelay {
0%, 70%, 100% {
-webkit-transform: scale3D(1, 1, 1);
transform: scale3D(1, 1, 1);
} 35% {
-webkit-transform: scale3D(0, 0, 1);
transform: scale3D(0, 0, 1);
}
}
@keyframes sk-cubeGridScaleDelay {
0%, 70%, 100% {
-webkit-transform: scale3D(1, 1, 1);
transform: scale3D(1, 1, 1);
} 35% {
-webkit-transform: scale3D(0, 0, 1);
transform: scale3D(0, 0, 1);
}
}

78
src/themes.ts

@ -0,0 +1,78 @@
import { ThemeCollection, ColorScheme } from 'src/types'
const themes: ThemeCollection = {
'blue': {
[ColorScheme.Light]: {
primary: '#3b42f4',
primaryAlternate: '#fff',
secondary: '#6165e0',
backgroundPrimary: '#fff',
backgroundSecondary: '#e6eeff',
text: '#555',
red: '#ff1a1a',
green: '#00802b',
blue: '#005ce6',
},
[ColorScheme.Dark]: {
primary: '#e5e6fe',
primaryAlternate: '#3b42f4',
secondary: '#fff',
text: '#ddd',
backgroundPrimary: '#000',
backgroundSecondary: '#333',
red: '#ff1a1a',
green: '#00802b',
blue: '#005ce6',
},
},
'orange': {
[ColorScheme.Light]: {
primary: '#ff8000',
primaryAlternate: '#fff',
secondary: '#ffb84d',
backgroundPrimary: '#fff',
backgroundSecondary: '#ffe6cc',
text: '#555',
red: '#ff1a1a',
green: '#00802b',
blue: '#005ce6',
},
[ColorScheme.Dark]: {
primary: '#ffe6cc',
primaryAlternate: '#ff8000',
secondary: '#fff',
text: '#ddd',
backgroundPrimary: '#000',
backgroundSecondary: '#333',
red: '#ff1a1a',
green: '#00802b',
blue: '#005ce6',
},
},
'green': {
[ColorScheme.Light]: {
primary: '#004d00',
primaryAlternate: '#fff',
secondary: '#336600',
backgroundPrimary: '#fff',
backgroundSecondary: '#e6ffe6',
text: '#555',
red: '#ff1a1a',
green: '#00802b',
blue: '#005ce6',
},
[ColorScheme.Dark]: {
primary: '#e6ffe6',
primaryAlternate: '#39ac39',
secondary: '#fff',
text: '#ddd',
backgroundPrimary: '#000',
backgroundSecondary: '#333',
red: '#ff1a1a',
green: '#00802b',
blue: '#005ce6',
},
},
}
export default themes

23
src/types/index.ts

@ -30,6 +30,29 @@ export interface SasResponse {
id: string
}
export interface LevelItem {
label?: string
content: string
}
export interface Theme {
primary: string
primaryAlternate: string
secondary: string
backgroundPrimary: string
backgroundSecondary: string
text: string
red: string
green: string
blue: string
}
export interface ThemeCollection {
[name: string]: {
[scheme: string]: Theme
}
}
export * from './config'
export * from './entities'
export * from './store'

17
src/types/store.ts

@ -36,6 +36,7 @@ export enum RequestKey {
Subscribe = 'subscribe',
UninstallApp = 'uninstall-app',
Unsubscribe = 'unsubscribe',
UpdateApp = 'update-app',
UpdateGroup = 'update-group',
UpdateSelf = 'update-self',
}
@ -82,10 +83,6 @@ export interface AuthenticationState {
userId?: string
}
export interface MenuState {
collapsed: boolean
}
export interface FormField {
name: string
apiName?: string
@ -119,6 +116,16 @@ export type ComposerState = {
height: number
}
export enum ColorScheme {
Light = 'light',
Dark = 'dark',
}
export type ThemeState = {
scheme: ColorScheme
name: string
}
export interface EntityListCollection {
[key: string]: EntityList
}
@ -136,8 +143,8 @@ export interface AppState {
entities: EntitiesState
forms: FormsState
lists: EntityListsState
menu: MenuState
notifications: NotificationsState
registration: RegistrationState
requests: RequestsState
theme: ThemeState
}

5
src/utils/index.ts

@ -5,6 +5,7 @@ import {
Form,
FormValue,
Config,
ClassDictionary,
} from 'src/types'
export function notificationTypeToClassName(type: NotificationType): string {
@ -20,7 +21,7 @@ export const objectToQuerystring = (obj: object) => Object.entries(obj).filter((
export function setTitle(title: string, decorate: boolean = true) {
if (decorate) {
document.title = `${title} / Flexor`
document.title = `${title} - Flexor`
} else {
document.title = title
}
@ -50,3 +51,5 @@ export function getOrigin(url: string) {
parser.href = url
return parser.origin
}
export const classNames = (dictionary: ClassDictionary) => Object.entries(dictionary).filter(([_, value]) => !!value).map(([key, _]) => key).join(' ')

22
webpack.config.ts

@ -1,7 +1,8 @@
import { Configuration } from 'webpack'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import postcssPresetEnv from 'postcss-preset-env'
// import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import c from './config/config.json'
@ -25,7 +26,7 @@ const config: Configuration = {
contentBase: `${__dirname}/dist`,
historyApiFallback: true,
before: app => {
app.get('/config.json', (req, res) => {
app.get('/config.json', (_, res) => {
res.json(c)
})
},
@ -44,16 +45,25 @@ const config: Configuration = {
use: 'ts-loader',
},
{
test: /\.scss$/,
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'sass-loader',
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
options: {
plugins: [
postcssPresetEnv({
stage: 2,
}),
]
}
}
],
},
],

Loading…
Cancel
Save