Dwayne Harris 5 years ago
parent
commit
a494d5ca14
  1. 100
      package-lock.json
  2. 11
      package.json
  3. 11
      src/components/app.tsx
  4. 43
      src/components/controls/file-field.tsx
  5. 19
      src/components/controls/theme-field.tsx
  6. 4
      src/components/create-user-step.tsx
  7. 31
      src/components/group-invitations.tsx
  8. 10
      src/components/group-logs.tsx
  9. 6
      src/components/member-list-item.tsx
  10. 2
      src/components/member-list.tsx
  11. 21
      src/components/pages/about.tsx
  12. 30
      src/components/pages/developers.tsx
  13. 87
      src/components/pages/group-admin.tsx
  14. 4
      src/components/pages/groups.tsx
  15. 6
      src/components/pages/register-group.tsx
  16. 10
      src/components/pages/register.tsx
  17. 2
      src/components/pages/self.tsx
  18. 15
      src/components/pages/view-group.tsx
  19. 17
      src/components/pages/view-user.tsx
  20. 17
      src/components/progress.tsx
  21. 2
      src/components/section.tsx
  22. 4
      src/components/self-info.tsx
  23. 3
      src/reducers/theme.ts
  24. 51
      src/styles/app.css
  25. 54
      src/themes.ts
  26. 2
      src/types/entities.ts
  27. 3
      src/utils/index.ts

100
package-lock.json

@ -76,22 +76,6 @@
"tslib": "^1.9.3"
}
},
"@azure/identity": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.0.0.tgz",
"integrity": "sha512-IyxddNpAlvLYmtNgzedYhJPnlmJpfyJ3S+fmiVHWhCMLvJI3x1011noZu8wyJektQN7NIANKDD77H4OLjf+DUQ==",
"requires": {
"@azure/core-http": "^1.0.0",
"@azure/core-tracing": "1.0.0-preview.5",
"@azure/logger": "^1.0.0",
"events": "^3.0.0",
"jws": "^3.2.2",
"msal": "^1.0.2",
"qs": "^6.7.0",
"tslib": "^1.9.3",
"uuid": "^3.3.2"
}
},
"@azure/logger": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.0.tgz",
@ -310,9 +294,9 @@
}
},
"@types/lodash": {
"version": "4.14.146",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.146.tgz",
"integrity": "sha512-JzJcmQ/ikHSv7pbvrVNKJU5j9jL9VLf3/gqs048CEnBVVVEv4kve3vLxoPHGvclutS+Il4SBIuQQ087m1eHffw==",
"version": "4.14.148",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.148.tgz",
"integrity": "sha512-05+sIGPev6pwpHF7NZKfP3jcXhXsIVFnYyVRT4WOB0me62E8OlWfTN+sKyt2/rqN+ETxuHAtgTSK1v71F0yncg==",
"dev": true
},
"@types/mime": {
@ -499,9 +483,9 @@
}
},
"@types/webpack": {
"version": "4.39.8",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.39.8.tgz",
"integrity": "sha512-lkJvwNJQUPW2SbVwAZW9s9whJp02nzLf2yTNwMULa4LloED9MYS1aNnGeoBCifpAI1pEBkTpLhuyRmBnLEOZAA==",
"version": "4.39.9",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.39.9.tgz",
"integrity": "sha512-p6GjQ+x86XyQd32IS3rYlisEpkuP8/DCW87mxpzAUs4McmKus7mD+cCEDaMIJIFfLW6cyYHfD+xEo/xREpT9lA==",
"dev": true,
"requires": {
"@types/anymatch": "*",
@ -1388,11 +1372,6 @@
"isarray": "^1.0.0"
}
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
@ -2359,14 +2338,6 @@
"stream-shift": "^1.0.0"
}
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -4623,25 +4594,6 @@
"minimist": "^1.2.0"
}
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"requires": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"requires": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@ -5060,14 +5012,6 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"msal": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/msal/-/msal-1.1.3.tgz",
"integrity": "sha512-cdShb+N1H3OyR1y46ij6OO7QzeqC6BxrbrNcouS4JBrr1+DnZ55TumxQKEzWmTXHvsbsuz5PCyXZl812Un8L9g==",
"requires": {
"tslib": "^1.9.3"
}
},
"multicast-dns": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
@ -6374,7 +6318,8 @@
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"dev": true
},
"query-string": {
"version": "4.3.4",
@ -6442,9 +6387,9 @@
}
},
"react": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.11.0.tgz",
"integrity": "sha512-M5Y8yITaLmU0ynd0r1Yvfq98Rmll6q8AxaEe88c8e7LxO8fZ2cNgmFt0aGAS9wzf1Ao32NKXtCl+/tVVtkxq6g==",
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
"integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@ -6452,14 +6397,14 @@
}
},
"react-dom": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.11.0.tgz",
"integrity": "sha512-nrRyIUE1e7j8PaXSPtyRKtz+2y9ubW/ghNgqKFHHAHaeP0fpF5uXR+sq8IMRHC+ZUxw7W9NyCDTBtwWxvkb0iA==",
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz",
"integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.17.0"
"scheduler": "^0.18.0"
}
},
"react-is": {
@ -6892,7 +6837,8 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"safe-regex": {
"version": "1.1.0",
@ -6921,9 +6867,9 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"scheduler": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.17.0.tgz",
"integrity": "sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz",
"integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@ -7786,9 +7732,9 @@
}
},
"ts-node": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.0.tgz",
"integrity": "sha512-fbG32iZEupNV2E2Fd2m2yt1TdAwR3GTCrJQBHDevIiEBNy1A8kqnyl1fv7jmRmmbtcapFab2glZXHJvfD1ed0Q==",
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.2.tgz",
"integrity": "sha512-W1DK/a6BGoV/D4x/SXXm6TSQx6q3blECUzd5TN+j56YEMX3yPVMpHsICLedUw3DvGF3aTQ8hfdR9AKMaHjIi+A==",
"dev": true,
"requires": {
"arg": "^4.1.0",

11
package.json

@ -21,7 +21,7 @@
},
"devDependencies": {
"@types/html-webpack-plugin": "^3.2.1",
"@types/lodash": "^4.14.146",
"@types/lodash": "^4.14.148",
"@types/mini-css-extract-plugin": "^0.8.0",
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.4",
@ -29,7 +29,7 @@
"@types/react-router-dom": "^5.1.2",
"@types/redux-logger": "^3.0.7",
"@types/uuid": "^3.4.6",
"@types/webpack": "^4.39.8",
"@types/webpack": "^4.39.9",
"@types/webpack-bundle-analyzer": "^2.13.3",
"@types/webpack-dev-server": "^3.4.0",
"@types/zxcvbn": "^4.4.0",
@ -43,7 +43,7 @@
"postcss-preset-env": "^6.7.0",
"style-loader": "^1.0.0",
"ts-loader": "^6.2.1",
"ts-node": "^8.5.0",
"ts-node": "^8.5.2",
"typescript": "^3.7.2",
"webpack": "^4.41.2",
"webpack-bundle-analyzer": "^3.6.0",
@ -51,7 +51,6 @@
"webpack-dev-server": "^3.9.0"
},
"dependencies": {
"@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",
@ -60,8 +59,8 @@
"history": "^4.10.1",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-redux": "^7.1.3",
"react-router-dom": "^5.1.2",
"redux": "^4.0.4",

11
src/components/app.tsx

@ -5,12 +5,14 @@ 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 { setTheme } from 'src/actions/theme'
import { getAuthenticatedUser } from 'src/selectors/authentication'
import { getFetching } from 'src/selectors'
import getConfig from 'src/config'
import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from 'src/constants'
import { useDeepCompareEffect, useTheme } from 'src/hooks'
import { AppState, AppThunkDispatch } from 'src/types'
import { AppState, AppThunkDispatch, User } from 'src/types'
import Footer from './footer'
import Logo from './logo'
@ -41,6 +43,7 @@ import '../styles/app.css'
const App: FC = () => {
const theme = useTheme()
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const fetching = useSelector<AppState, boolean>(getFetching)
const dispatch = useDispatch<AppThunkDispatch>()
@ -63,6 +66,10 @@ const App: FC = () => {
init()
}, [])
useDeepCompareEffect(() => {
if (user && user.theme) dispatch(setTheme(user.theme))
}, [user])
useDeepCompareEffect(() => {
document.body.style.backgroundColor = theme.backgroundPrimary
}, [theme])
@ -74,7 +81,7 @@ const App: FC = () => {
<Logo />
</div>
<div className="content" style={{ borderLeftColor: theme.backgroundSecondary, borderRightColor: theme.backgroundPrimary }}>
<div className="content" style={{ backgroundColor: theme.backgroundPrimary, borderLeftColor: theme.backgroundSecondary, borderRightColor: theme.backgroundPrimary }}>
<Switch>
<Route path="/c/:id/admin/:tab?">
<GroupAdmin />

43
src/components/controls/file-field.tsx

@ -2,16 +2,16 @@ import React, { FC, ChangeEvent, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUpload } from '@fortawesome/free-solid-svg-icons'
import { DefaultAzureCredential } from '@azure/identity'
import { BlockBlobClient } from '@azure/storage-blob'
import { BlockBlobClient, AnonymousCredential } from '@azure/storage-blob'
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, SasResponse, NotificationType } from 'src/types'
import { AppState, AppThunkDispatch, SasResponse, NotificationType } from 'src/types'
import Progress from 'src/components/progress'
import FieldLabel from 'src/components/controls/field-label'
interface Props {
@ -26,7 +26,7 @@ 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()
const dispatch = useDispatch<AppThunkDispatch>()
const { name, label, help, previewWidth = 128, maxSize = config.media.defaultMaxSize } = props
@ -50,33 +50,32 @@ const FileField: FC<Props> = props => {
setUploading(true)
const defaultAzureCredential = new DefaultAzureCredential()
const blockBlobClient = new BlockBlobClient(`${config.blobUrl}${filename}?${sas}`, defaultAzureCredential)
try {
const blockBlobClient = new BlockBlobClient(`${config.blobUrl}${filename}?${sas}`, new AnonymousCredential())
await blockBlobClient.uploadBrowserData(file, {
onProgress: p => {
setProgress((p.loadedBytes / file.size) * 100)
}
})
await apiFetch({
path: '/api/media',
method: 'post',
body: {
name: filename,
size: file.size,
type: file.type,
originalName: file.name,
}
})
dispatch(setFieldValue(name, filename))
setUploaded(true)
} catch (err) {
console.error(err)
dispatch(showNotification(NotificationType.Error, `Upload error: ${err}`))
}
await apiFetch({
path: '/api/media',
method: 'post',
body: {
name: filename,
size: file.size,
type: file.type,
originalName: file.name,
}
})
dispatch(setFieldValue(name, filename))
setUploaded(true)
setUploading(false)
}
}
@ -97,9 +96,9 @@ const FileField: FC<Props> = props => {
if (uploading) {
return (
<div>
<div className="field">
<FieldLabel>{label}</FieldLabel>
<progress value={progress} max="100">{progress}%</progress>
<Progress value={progress} />
</div>
)
}

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

@ -17,10 +17,9 @@ export interface Props {
}
const ThemeField: FC<Props> = ({ name, label }) => {
const currentTheme = useTheme()
const currentThemeName = useSelector<AppState, string>(getThemeName)
const [previousThemeName, setPreviousThemeName] = useState('')
const theme = useTheme()
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, ''))
const [displayedName, setDisplayedName] = useState(value)
const dispatch = useDispatch()
const themeList = Object.entries(themes).map(([name, schemes]) => {
@ -29,30 +28,32 @@ const ThemeField: FC<Props> = ({ name, label }) => {
const handleMouseEnter = (name: string) => {
dispatch(setTheme(name))
setDisplayedName(name)
}
const handleMouseLeave = () => {
dispatch(setTheme(previousThemeName))
dispatch(setTheme(value))
setDisplayedName(value)
}
useEffect(() => {
setPreviousThemeName(currentThemeName)
}, [])
setDisplayedName(value)
}, [value])
return (
<div className="field">
<FieldLabel>{label}</FieldLabel>
<div className="control">
<div className="theme-picker">
{themeList.map(([themeName, theme]) => (
{themeList.map(([themeName, t]) => (
<div
style={{ backgroundColor: theme.primary, borderColor: themeName === value ? currentTheme.red : currentTheme.secondary }}
style={{ backgroundColor: t.primary, borderColor: themeName === value ? t.red : '#222' }}
onMouseEnter={() => handleMouseEnter(themeName)}
onMouseLeave={() => handleMouseLeave()}
onClick={() => dispatch(setFieldValue(name, themeName))}></div>
))}
</div>
<div style={{ color: currentTheme.primary }}>{capitalize(currentThemeName)}</div>
<div style={{ color: theme.primary }}>{capitalize(displayedName)}</div>
</div>
</div>
)

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

@ -75,9 +75,9 @@ const CreateUserStep: FC = () => {
<CreateUserForm />
<HorizontalRule />
<nav className="level">
<nav className="level" style={{ flexDirection: 'row-reverse'}}>
<div>
<PrimaryButton text="Community" onClick={() => next()} />
<PrimaryButton text="To Community" onClick={() => next()} />
</div>
</nav>
</div>

31
src/components/group-invitations.tsx

@ -1,10 +1,10 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Link, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheckCircle, faStopwatch, faPauseCircle } from '@fortawesome/free-solid-svg-icons'
import moment from 'moment'
import { useTheme } from 'src/hooks'
import { handleApiError } from 'src/api/errors'
import { fetchInvitations, createInvitation } from 'src/actions/groups'
import { getInvitations } from 'src/selectors/groups'
@ -12,7 +12,7 @@ import { getFieldValue } from 'src/selectors/forms'
import { AppState, Invitation, AppThunkDispatch } from 'src/types'
import Title from 'src/components/title'
import PrimaryButton from 'src/components/controls/primary-button'
import Subtitle from 'src/components/subtitle'
import SelectField from 'src/components/controls/select-field'
@ -21,6 +21,7 @@ interface Props {
}
const GroupInvitations: FC<Props> = ({ group }) => {
const theme = useTheme()
const invitations = useSelector<AppState, Invitation[]>(getInvitations)
const expiration = useSelector<AppState, string>(state => getFieldValue<string>(state, 'expiration', '0'))
const limit = useSelector<AppState, string>(state => getFieldValue<string>(state, 'limit', '0'))
@ -63,29 +64,17 @@ const GroupInvitations: FC<Props> = ({ group }) => {
return (
<div>
<Title>Invitations</Title>
<Subtitle>Create an invitation for someone to create a new account in this Community.</Subtitle>
<Subtitle>Invitations</Subtitle>
<p style={{ color: theme.text }}>Create an invitation for someone to create a new account in this Community.</p>
<div className="invitation-options">
<SelectField name="expiration" label="Expires" options={expirationOptions} icon={faStopwatch} />
<SelectField name="limit" label="Uses" options={limitOptions} icon={faPauseCircle} />
<div className="field">
<div className="label">&nbsp;</div>
<div className="control">
<button className="button is-primary" onClick={() => handleCreateInvitation()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>
<span>Create</span>
</button>
</div>
</div>
</div>
<SelectField name="expiration" label="Expires" options={expirationOptions} icon={faStopwatch} />
<SelectField name="limit" label="Uses" options={limitOptions} icon={faPauseCircle} />
<PrimaryButton text="Create" icon={faCheckCircle} onClick={() => handleCreateInvitation()} />
<br />
{invitations.length > 0 &&
<table className="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
<table>
<thead>
<tr>
<th>Code</th>

10
src/components/group-logs.tsx

@ -6,6 +6,7 @@ import moment from 'moment'
import { handleApiError } from 'src/api/errors'
import { fetchLogs } from 'src/actions/groups'
import { getLogs } from 'src/selectors/groups'
import { useTheme } from 'src/hooks'
import { AppState, GroupLog } from 'src/types'
interface Props {
@ -13,6 +14,7 @@ interface Props {
}
const MemberList: FC<Props> = ({ group }) => {
const theme = useTheme()
const logs = useSelector<AppState, GroupLog[]>(getLogs)
const dispatch = useDispatch()
@ -28,8 +30,8 @@ const MemberList: FC<Props> = ({ group }) => {
return (
<table>
<thead>
<tr>
<thead style={{ backgroundColor: theme.primary }}>
<tr style={{ color: theme.primaryAlternate, fontWeight: 700 }}>
<th>Who</th>
<th>What</th>
<th>When</th>
@ -37,8 +39,8 @@ const MemberList: FC<Props> = ({ group }) => {
</thead>
<tbody>
{logs.map(log => (
<tr>
<td><Link to={`/u/${log.user.id}`}>{log.user.id}</Link></td>
<tr style={{ backgroundColor: theme.backgroundSecondary }}>
<td><Link style={{ color: theme.primary }} to={`/u/${log.user.id}`}>{log.user.id}</Link></td>
<td>{log.content}</td>
<td>{moment(log.created).format('MMMM Do YYYY, h:mm:ss a')}</td>
</tr>

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

@ -21,12 +21,12 @@ const MemberListItem: FC<Props> = ({ member }) => {
}
return (
<div className="member">
<Link to={`/u/${member.id}`} style={{ color: theme.primary, fontSize: '1.1rem' }}>{member.name}</Link>
<div className="member" style={{ backgroundColor: theme.backgroundSecondary }}>
<Link to={`/u/${member.id}`} style={{ color: theme.primary, fontSize: '1rem' }}>{member.name}</Link>
<br />
<Link to={`/u/${member.id}`} style={{ color: theme.secondary, fontSize: '0.9rem' }}>@{member.id}</Link>
<br />
<span className="tag" style={{ color: tagColor() }}>{capitalize(member.membership as string)}</span>
<span className="tag" style={{ backgroundColor: tagColor(), color: '#fff' }}>{capitalize(member.membership as string)}</span>
</div>
)
}

2
src/components/member-list.tsx

@ -27,7 +27,7 @@ const MemberList: FC<Props> = ({ group }) => {
}, [group])
return (
<div className="is-flex">
<div style={{ display: 'flex' }}>
{members.map(member => <MemberListItem key={member.id} member={member} />)}
</div>
)

21
src/components/pages/about.tsx

@ -1,10 +1,15 @@
import React, { FC, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useTheme } from 'src/hooks'
import { setTitle } from 'src/utils'
import Section from 'src/components/section'
import Title from 'src/components/title'
import Subtitle from 'src/components/subtitle'
const About: FC = () => {
const theme = useTheme()
useEffect(() => {
setTitle('About Flexor', false)
})
@ -14,9 +19,19 @@ const About: FC = () => {
<Section>
<Title>About Flexor</Title>
<p>
Flexor is a website.
</p>
<p>Flexor is a service that lets users post stuff for their subscribers to see. Here are some things to know about how it works:</p>
<Subtitle>Communities</Subtitle>
<p>Flexor is made up of Communities. Each account is created through one. Communities enforce their own standards of behavior.</p>
<p>Check out <Link style={{ color: theme.secondary }} to="/groups">the list of Communities</Link>.</p>
<Subtitle>Apps</Subtitle>
<p>Users post content to Flexor through apps created by other people/organizations.</p>
<p>Check out <Link style={{ color: theme.secondary }} to="/apps">the list of Apps</Link>.</p>
</Section>
</div>
)

30
src/components/pages/developers.tsx

@ -31,13 +31,31 @@ const Developers: FC = () => {
<Subtitle>Developer Documentation</Subtitle>
<HorizontalRule />
<p>Flexor apps allow users to express themselves on the network.</p>
<br />
<p>Developer documentation coming soon.</p>
<HorizontalRule />
<p>Flexor Apps let Users post stuff to the service.</p>
<p>Each App has two parts:</p>
<Subtitle>Composer</Subtitle>
<p>The Composer is the interface for creating a post. It can be anything that results in a post object being made.</p>
<p>
The <strong>composerURL</strong> field of an app should point to an HTML page that will be rendered in an iFrame in the Flexor app.
Communication between the Composer page and the Flexor app is done via Javascript <strong>postMessage</strong> messages.
</p>
<p>This is where you manage apps you create.</p>
<br />
<Subtitle>Renderer</Subtitle>
<p>
The Renderer is the interface for displaying a post.
This is only used on the view Post page and is optional.
The default Flexor renderer is used when displaying a post elsewhere or when the <strong>rendererURL</strong> field is empty.
</p>
<p>
The <strong>rendererURL</strong> field of an app should point to an HTML page that will be rendered in an iFrame in the Flexor app.
</p>
<HorizontalRule />
<PrimaryButton text="Create a new App" icon={faPlusCircle} onClick={() => history.push('/developers/create')} />
</Section>

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

@ -2,7 +2,7 @@ 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 { faCheckCircle } from '@fortawesome/free-solid-svg-icons'
import { faDoorOpen, faCheckCircle, faIdCard, faEnvelope, faUserShield } from '@fortawesome/free-solid-svg-icons'
import { handleApiError } from 'src/api/errors'
import { initForm, initField } from 'src/actions/forms'
@ -10,7 +10,7 @@ import { fetchGroup, updateGroup } from 'src/actions/groups'
import { getEntity } from 'src/selectors/entities'
import { getForm } from 'src/selectors/forms'
import { useDeepCompareEffect } from 'src/hooks'
import { useDeepCompareEffect, useTheme } from 'src/hooks'
import { setTitle, valueFromForm } from 'src/utils'
import {
AppState,
@ -24,7 +24,9 @@ import {
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 MemberList from 'src/components/member-list'
import GroupInvitations from 'src/components/group-invitations'
import GroupLogs from 'src/components/group-logs'
@ -48,6 +50,7 @@ const tabs: Tab[] = [
]
const GroupAdmin: FC = () => {
const theme = useTheme()
const { id, tab = '' } = useParams<Params>()
const history = useHistory()
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
@ -101,58 +104,60 @@ const GroupAdmin: FC = () => {
return (
<div>
<Title>{group.name}</Title>
<Subtitle>Administration</Subtitle>
<HorizontalRule />
<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>
<Section>
<Title>{group.name}</Title>
<Subtitle>Administration</Subtitle>
<HorizontalRule />
<div className="tabs" style={{ backgroundColor: theme.backgroundSecondary }}>
{tabs.map(t => (
<div key={t.id} className={tab === t.id ? 'active': ''} style={{ borderColor: theme.primary }}>
<Link style={{ color: theme.secondary}} to={`/c/${group.id}/admin/${t.id}`}>
{t.label}
</Link>
</div>
))}
</div>
<div className="container">
<div>
{tab === '' &&
<div>
<div className="field">
<FieldLabel>ID</FieldLabel>
<div className="control">
<input className="input" type="text" value={group.id} readOnly />
<div className="control-container">
<div className="icon" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<FontAwesomeIcon icon={faIdCard} />
</div>
<div className="control">
<input
style={{ backgroundColor: theme.backgroundSecondary, borderColor: theme.secondary, color: theme.text }}
type="text"
value={group.id}
readOnly />
</div>
</div>
</div>
<br />
<div className="field">
<FieldLabel>Name</FieldLabel>
<div className="control">
<input className="input" type="text" value={group.name} readOnly />
<div className="control-container">
<div className="control">
<input
style={{ backgroundColor: theme.backgroundSecondary, borderColor: theme.secondary, color: theme.text }}
type="text"
value={group.name}
readOnly />
</div>
</div>
</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>
<br />
<PrimaryButton text="Save" icon={faCheckCircle} onClick={e => handleUpdateGroup()} />
</div>
}
@ -161,14 +166,18 @@ const GroupAdmin: FC = () => {
<GroupInvitations group={id} />
<HorizontalRule />
<Title>Members</Title>
<Subtitle>Members</Subtitle>
<MemberList group={id} />
</div>
}
{tab === 'logs' && <GroupLogs group={id} />}
{tab === 'logs' &&
<div>
<GroupLogs group={id} />
</div>
}
</div>
</div>
</Section>
</div>
)
}

4
src/components/pages/groups.tsx

@ -29,6 +29,10 @@ const Groups: FC = () => {
<Section>
<Title>Communities</Title>
<p>Flexor is made up of Communities. Each User account is created through one.</p>
<HorizontalRule />
{groups.map(group => <GroupListItem group={group} />)}
<HorizontalRule />

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

@ -11,7 +11,7 @@ import { register } from 'src/actions/registration'
import { getEntity } from 'src/selectors/entities'
import { getForm } from 'src/selectors/forms'
import { setTitle, valueFromForm } from 'src/utils'
import { setTitle, valueFromForm, getDefaultThemeName } from 'src/utils'
import { useDeepCompareEffect } from 'src/hooks'
import { AppState, AppThunkDispatch, Group, EntityType, NotificationType, Form } from 'src/types'
@ -46,7 +46,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-theme', getDefaultThemeName(), 'theme'))
dispatch(initField('user-image', '', 'imageUrl'))
dispatch(initField('user-cover-image', '', 'coverImageUrl'))
dispatch(initField('user-agree', false))
@ -75,7 +75,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', ''),
theme: valueFromForm<string>(form, 'user-theme', getDefaultThemeName()),
}))
dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`))

10
src/components/pages/register.tsx

@ -8,7 +8,7 @@ import { getStep } from 'src/selectors/registration'
import { initForm, initField } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications'
import { createGroup, register } from 'src/actions/registration'
import { setTitle, valueFromForm } from 'src/utils'
import { setTitle, valueFromForm, getDefaultThemeName } from 'src/utils'
import { AppState, AppThunkDispatch, Form, NotificationType } from 'src/types'
import Title from 'src/components/title'
@ -43,7 +43,7 @@ const Register: FC = () => {
coverImageUrl: valueFromForm<string>(form, 'user-cover-image', ''),
requiresApproval: valueFromForm<boolean>(form, 'user-requires-approval', false),
privacy: valueFromForm<string>(form, 'user-privacy', 'open'),
theme: valueFromForm<string>(form, 'user-theme', ''),
theme: valueFromForm<string>(form, 'user-theme', getDefaultThemeName()),
}))
await dispatch(createGroup({
@ -52,7 +52,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', ''),
theme: valueFromForm<string>(form, 'group-theme', getDefaultThemeName()),
}))
dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`))
@ -84,7 +84,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-theme', getDefaultThemeName(), 'theme'))
dispatch(initField('group-agree', false))
dispatch(initField('user-id', '', 'id'))
dispatch(initField('user-name', '', 'name'))
@ -94,7 +94,7 @@ const Register: FC = () => {
dispatch(initField('user-cover-image', '', 'coverImageUrl'))
dispatch(initField('user-requires-approval', false, 'requiresApproval'))
dispatch(initField('user-privacy', 'public', 'privacy'))
dispatch(initField('user-theme', '', 'theme'))
dispatch(initField('user-theme', getDefaultThemeName(), 'theme'))
dispatch(initField('user-agree', false))
}, [])

2
src/components/pages/self.tsx

@ -132,7 +132,7 @@ const Self: FC = () => {
<TextField name="name" label="Name" placeholder="Your Display Name" />
<TextareaField name="about" label="About" placeholder="Your Bio" />
<ThemeField name="color" label="Color" />
<ThemeField name="theme" label="Color" />
<SelectField name="privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<ImageField name="image" label="Avatar" />
<CoverImageField name="coverImage" />

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

@ -1,4 +1,4 @@
import React, { FC, useEffect } from 'react'
import React, { FC, useEffect, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useParams, useHistory } from 'react-router-dom'
import { faEdit, faUserCheck, faBan } from '@fortawesome/free-solid-svg-icons'
@ -8,6 +8,7 @@ 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 { getThemeName } from 'src/selectors/theme'
import { useDeepCompareEffect, useConfig, useTheme } from 'src/hooks'
import { setTitle, urlForBlob } from 'src/utils'
@ -20,6 +21,7 @@ import PrimaryButton from 'src/components/controls/primary-button'
import Button from 'src/components/controls/button'
import Loading from 'src/components/pages/loading'
import HorizontalRule from 'src/components/horizontal-rule'
import { setTheme } from 'src/actions/theme'
interface Params {
id: string
@ -28,6 +30,8 @@ interface Params {
const ViewGroup: FC = () => {
const { id } = useParams<Params>()
const theme = useTheme()
const themeName = useSelector<AppState, string>(getThemeName)
const [selectedThemeName] = useState(themeName)
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const dispatch = useDispatch<AppThunkDispatch>()
@ -43,7 +47,14 @@ const ViewGroup: FC = () => {
}, [])
useDeepCompareEffect(() => {
if (group) setTitle(group.name)
if (group) {
setTitle(group.name)
dispatch(setTheme(group.theme))
}
return () => {
dispatch(setTheme(selectedThemeName))
}
}, [group])
if (!group) return <Loading />

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

@ -1,4 +1,4 @@
import React, { FC, useEffect } from 'react'
import React, { FC, useEffect, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useParams, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@ -8,13 +8,15 @@ import moment from 'moment'
import { handleApiError } from 'src/api/errors'
import { fetchUser, subscribe, unsubscribe } from 'src/actions/users'
import { fetchUserPosts } from 'src/actions/posts'
import { setTheme } from 'src/actions/theme'
import { getEntity } from 'src/selectors/entities'
import { getAuthenticatedUser, getChecked } from 'src/selectors/authentication'
import { getUserPosts } from 'src/selectors/posts'
import { getThemeName } from 'src/selectors/theme'
import { useDeepCompareEffect, useConfig, useTheme } from 'src/hooks'
import { setTitle, urlForBlob } from 'src/utils'
import { AppState, EntityType, User, Post, AppThunkDispatch, LevelItem } from 'src/types'
import { AppState, Theme, EntityType, User, Post, AppThunkDispatch, LevelItem } from 'src/types'
import Title from 'src/components/title'
import Subtitle from 'src/components/subtitle'
@ -31,6 +33,8 @@ interface Params {
const ViewUser: FC = () => {
const { id } = useParams<Params>()
const theme = useTheme()
const themeName = useSelector<AppState, string>(getThemeName)
const [selectedThemeName] = useState(themeName)
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))
@ -53,7 +57,14 @@ const ViewUser: FC = () => {
}, [checked])
useDeepCompareEffect(() => {
if (user) setTitle(user.name)
if (user) {
setTitle(user.name)
dispatch(setTheme(user.theme))
}
return () => {
dispatch(setTheme(selectedThemeName))
}
}, [user])
if (!user) return <Loading />

17
src/components/progress.tsx

@ -0,0 +1,17 @@
import React, { FC } from 'react'
import { useTheme } from 'src/hooks'
interface Props {
value: number
}
const Progress: FC<Props> = ({ value }) => {
const theme = useTheme()
return (
<div className="progress" style={{ backgroundColor: theme.backgroundSecondary, borderColor: theme.primary }}>
<div style={{ backgroundColor: theme.secondary, width: `${value}%`}}></div>
</div>
)
}
export default Progress

2
src/components/section.tsx

@ -4,7 +4,7 @@ import { useTheme } from 'src/hooks'
const Section: FC = ({ children }) => {
const theme = useTheme()
return (
<section style={{ backgroundColor: theme.backgroundPrimary }}>
<section style={{ backgroundColor: theme.backgroundPrimary, color: theme.text }}>
{children}
</section>
)

4
src/components/self-info.tsx

@ -31,12 +31,12 @@ const SelfInfo: FC = () => {
if (user.name) {
return (
<Link to="/self" style={{ color: theme.primaryAlternate }}>
<span style={{ fontSize: '1.1rem' }}>{user.name}</span> <span style={{ fontSize: '1rem', fontWeight: 'bold' }}>@{user.id}</span>
<span style={{ fontSize: '1rem' }}>{user.name}</span> <span style={{ fontSize: '0.9rem', fontWeight: 'bold' }}>@{user.id}</span>
</Link>
)
}
return <Link to="/self" className="is-size-4 has-text-white-ter">@{user.id}</Link>
return <Link to="/self" style={{ color: theme.primaryAlternate }}>@{user.id}</Link>
}
return (

3
src/reducers/theme.ts

@ -1,11 +1,12 @@
import { Reducer } from 'redux'
import { ThemeActions } from '../actions/theme'
import { getDefaultThemeName } from '../utils'
import { ThemeState, ColorScheme } from '../types'
const initialState: ThemeState = {
scheme: ColorScheme.Light,
name: 'blue',
name: getDefaultThemeName(),
}
const reducer: Reducer<ThemeState, ThemeActions> = (state = initialState, action) => {

51
src/styles/app.css

@ -34,7 +34,7 @@ input, textarea, select {
border-radius: 0;
box-sizing: border-box;
font-family: var(--default-font);
font-size: 1rem;
font-size: 0.9rem;
margin: 0px;
padding: var(--input-padding);
width: 100%;
@ -175,6 +175,54 @@ footer {
text-align: center;
}
table {
margin-top: 1rem;
width: 100%;
}
table tr:nth-child(even) {
background-color: transparent !important;
}
table td {
padding: 0.25rem;
}
span.tag {
border-radius: 10px;
display: inline-block;
font-size: 0.75rem;
padding: 5px;
}
div.tabs {
border-radius: 20px;
display: flex;
font-size: 1rem;
justify-content: space-around;
}
div.tabs > div {
padding: 0.5rem;
}
div.tabs > div.active {
border-bottom: 3px solid;
}
div.progress {
border: 1px solid;
height: 1rem;
margin: 1rem;
max-height: 100px;
min-height: 10px;
padding: 0px;
}
div.progress > div {
height: 100%;
}
div.field {
margin: 2rem 0px;
}
@ -266,7 +314,6 @@ p.label + p.content {
}
div.member {
border: var(--default-border);
margin-right: 10px;
min-width: 150px;
padding: 1rem;

54
src/themes.ts

@ -1,7 +1,7 @@
import { ThemeCollection, ColorScheme } from 'src/types'
const themes: ThemeCollection = {
'blue': {
'blueish': {
[ColorScheme.Light]: {
primary: '#3b42f4',
primaryAlternate: '#fff',
@ -25,7 +25,7 @@ const themes: ThemeCollection = {
blue: '#005ce6',
},
},
'orange': {
'orangeish': {
[ColorScheme.Light]: {
primary: '#ff8000',
primaryAlternate: '#fff',
@ -49,7 +49,7 @@ const themes: ThemeCollection = {
blue: '#005ce6',
},
},
'green': {
'greenish': {
[ColorScheme.Light]: {
primary: '#004d00',
primaryAlternate: '#fff',
@ -73,6 +73,54 @@ const themes: ThemeCollection = {
blue: '#005ce6',
},
},
'redish': {
[ColorScheme.Light]: {
primary: '#cc2900',
primaryAlternate: '#fff',
secondary: '#ff3333',
backgroundPrimary: '#fff',
backgroundSecondary: '#ffe6e6',
text: '#555',
red: '#ff1a1a',
green: '#00802b',
blue: '#005ce6',
},
[ColorScheme.Dark]: {
primary: '#ff8080',
primaryAlternate: '#fff',
secondary: '#ff3333',
text: '#ddd',
backgroundPrimary: '#000',
backgroundSecondary: '#333',
red: '#ff1a1a',
green: '#00802b',
blue: '#005ce6',
},
},
'black': {
[ColorScheme.Light]: {
primary: '#000',
primaryAlternate: '#ddd',
secondary: '#333',
backgroundPrimary: '#fff',
backgroundSecondary: '#eee',
text: '#555',
red: '#ff1a1a',
green: '#00802b',
blue: '#005ce6',
},
[ColorScheme.Dark]: {
primary: '#ccc',
primaryAlternate: '#333',
secondary: '#ddd',
text: '#ddd',
backgroundPrimary: '#000',
backgroundSecondary: '#333',
red: '#ff1a1a',
green: '#00802b',
blue: '#005ce6',
},
},
}
export default themes

2
src/types/entities.ts

@ -34,6 +34,7 @@ export type Group = Entity & {
imageUrl: string
coverImageUrl: string
iconImageUrl: string
theme: string
}
type BaseInstallation = Entity & {
@ -55,6 +56,7 @@ type BaseUser = Entity & {
about?: string
imageUrl?: string
coverImageUrl?: string
theme: string
requiresApproval: boolean
privacy: string
subscriptions: UserSubscription[]

3
src/utils/index.ts

@ -1,4 +1,5 @@
import getConfig from 'src/config'
import themes from 'src/themes'
import {
NotificationType,
@ -53,3 +54,5 @@ export function getOrigin(url: string) {
}
export const classNames = (dictionary: ClassDictionary) => Object.entries(dictionary).filter(([_, value]) => !!value).map(([key, _]) => key).join(' ')
export const getDefaultThemeName = () => Object.keys(themes)[0]
Loading…
Cancel
Save