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" "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": { "@azure/logger": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.0.tgz",
@ -310,9 +294,9 @@
} }
}, },
"@types/lodash": { "@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 "dev": true
}, },
"@types/mime": { "@types/mime": {
@ -499,9 +483,9 @@
} }
}, },
"@types/webpack": { "@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, "dev": true,
"requires": { "requires": {
"@types/anymatch": "*", "@types/anymatch": "*",
@ -1388,11 +1372,6 @@
"isarray": "^1.0.0" "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": { "buffer-from": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
@ -2359,14 +2338,6 @@
"stream-shift": "^1.0.0" "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": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -4623,25 +4594,6 @@
"minimist": "^1.2.0" "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": { "killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@ -5060,14 +5012,6 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true "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": { "multicast-dns": {
"version": "6.2.3", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
@ -6374,7 +6318,8 @@
"qs": { "qs": {
"version": "6.7.0", "version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "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": { "query-string": {
"version": "4.3.4", "version": "4.3.4",
@ -6442,9 +6387,9 @@
} }
}, },
"react": { "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": { "requires": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
@ -6452,14 +6397,14 @@
} }
}, },
"react-dom": { "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": { "requires": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"scheduler": "^0.17.0"
"scheduler": "^0.18.0"
} }
}, },
"react-is": { "react-is": {
@ -6892,7 +6837,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "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": { "safe-regex": {
"version": "1.1.0", "version": "1.1.0",
@ -6921,9 +6867,9 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
}, },
"scheduler": { "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": { "requires": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"object-assign": "^4.1.1" "object-assign": "^4.1.1"
@ -7786,9 +7732,9 @@
} }
}, },
"ts-node": { "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, "dev": true,
"requires": { "requires": {
"arg": "^4.1.0", "arg": "^4.1.0",

11
package.json

@ -21,7 +21,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/html-webpack-plugin": "^3.2.1", "@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/mini-css-extract-plugin": "^0.8.0",
"@types/react": "^16.9.11", "@types/react": "^16.9.11",
"@types/react-dom": "^16.9.4", "@types/react-dom": "^16.9.4",
@ -29,7 +29,7 @@
"@types/react-router-dom": "^5.1.2", "@types/react-router-dom": "^5.1.2",
"@types/redux-logger": "^3.0.7", "@types/redux-logger": "^3.0.7",
"@types/uuid": "^3.4.6", "@types/uuid": "^3.4.6",
"@types/webpack": "^4.39.8",
"@types/webpack": "^4.39.9",
"@types/webpack-bundle-analyzer": "^2.13.3", "@types/webpack-bundle-analyzer": "^2.13.3",
"@types/webpack-dev-server": "^3.4.0", "@types/webpack-dev-server": "^3.4.0",
"@types/zxcvbn": "^4.4.0", "@types/zxcvbn": "^4.4.0",
@ -43,7 +43,7 @@
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"ts-loader": "^6.2.1", "ts-loader": "^6.2.1",
"ts-node": "^8.5.0",
"ts-node": "^8.5.2",
"typescript": "^3.7.2", "typescript": "^3.7.2",
"webpack": "^4.41.2", "webpack": "^4.41.2",
"webpack-bundle-analyzer": "^3.6.0", "webpack-bundle-analyzer": "^3.6.0",
@ -51,7 +51,6 @@
"webpack-dev-server": "^3.9.0" "webpack-dev-server": "^3.9.0"
}, },
"dependencies": { "dependencies": {
"@azure/identity": "^1.0.0",
"@azure/storage-blob": "^12.0.0", "@azure/storage-blob": "^12.0.0",
"@fortawesome/fontawesome-common-types": "^0.2.25", "@fortawesome/fontawesome-common-types": "^0.2.25",
"@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/fontawesome-svg-core": "^1.2.25",
@ -60,8 +59,8 @@
"history": "^4.10.1", "history": "^4.10.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"moment": "^2.24.0", "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-redux": "^7.1.3",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"redux": "^4.0.4", "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 { handleApiError } from 'src/api/errors'
import { fetchSelf, setChecked } from 'src/actions/authentication' import { fetchSelf, setChecked } from 'src/actions/authentication'
import { setConfig } from 'src/actions/config' import { setConfig } from 'src/actions/config'
import { setTheme } from 'src/actions/theme'
import { getAuthenticatedUser } from 'src/selectors/authentication'
import { getFetching } from 'src/selectors' import { getFetching } from 'src/selectors'
import getConfig from 'src/config' import getConfig from 'src/config'
import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from 'src/constants' import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from 'src/constants'
import { useDeepCompareEffect, useTheme } from 'src/hooks' import { useDeepCompareEffect, useTheme } from 'src/hooks'
import { AppState, AppThunkDispatch } from 'src/types'
import { AppState, AppThunkDispatch, User } from 'src/types'
import Footer from './footer' import Footer from './footer'
import Logo from './logo' import Logo from './logo'
@ -41,6 +43,7 @@ import '../styles/app.css'
const App: FC = () => { const App: FC = () => {
const theme = useTheme() const theme = useTheme()
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const fetching = useSelector<AppState, boolean>(getFetching) const fetching = useSelector<AppState, boolean>(getFetching)
const dispatch = useDispatch<AppThunkDispatch>() const dispatch = useDispatch<AppThunkDispatch>()
@ -63,6 +66,10 @@ const App: FC = () => {
init() init()
}, []) }, [])
useDeepCompareEffect(() => {
if (user && user.theme) dispatch(setTheme(user.theme))
}, [user])
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
document.body.style.backgroundColor = theme.backgroundPrimary document.body.style.backgroundColor = theme.backgroundPrimary
}, [theme]) }, [theme])
@ -74,7 +81,7 @@ const App: FC = () => {
<Logo /> <Logo />
</div> </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> <Switch>
<Route path="/c/:id/admin/:tab?"> <Route path="/c/:id/admin/:tab?">
<GroupAdmin /> <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 { useSelector, useDispatch } from 'react-redux'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUpload } from '@fortawesome/free-solid-svg-icons' 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 { useConfig, useTheme } from 'src/hooks'
import { setFieldValue } from 'src/actions/forms' import { setFieldValue } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications' import { showNotification } from 'src/actions/notifications'
import { getFieldValue } from 'src/selectors/forms' import { getFieldValue } from 'src/selectors/forms'
import { apiFetch } from 'src/api/fetch' 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' import FieldLabel from 'src/components/controls/field-label'
interface Props { interface Props {
@ -26,7 +26,7 @@ const FileField: FC<Props> = props => {
const theme = useTheme() const theme = useTheme()
const value = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, props.name, false)) const value = useSelector<AppState, boolean>(state => getFieldValue<boolean>(state, props.name, false))
const config = useConfig() const config = useConfig()
const dispatch = useDispatch()
const dispatch = useDispatch<AppThunkDispatch>()
const { name, label, help, previewWidth = 128, maxSize = config.media.defaultMaxSize } = props const { name, label, help, previewWidth = 128, maxSize = config.media.defaultMaxSize } = props
@ -50,33 +50,32 @@ const FileField: FC<Props> = props => {
setUploading(true) setUploading(true)
const defaultAzureCredential = new DefaultAzureCredential()
const blockBlobClient = new BlockBlobClient(`${config.blobUrl}${filename}?${sas}`, defaultAzureCredential)
try { try {
const blockBlobClient = new BlockBlobClient(`${config.blobUrl}${filename}?${sas}`, new AnonymousCredential())
await blockBlobClient.uploadBrowserData(file, { await blockBlobClient.uploadBrowserData(file, {
onProgress: p => { onProgress: p => {
setProgress((p.loadedBytes / file.size) * 100) 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) { } catch (err) {
console.error(err) console.error(err)
dispatch(showNotification(NotificationType.Error, `Upload 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) setUploading(false)
} }
} }
@ -97,9 +96,9 @@ const FileField: FC<Props> = props => {
if (uploading) { if (uploading) {
return ( return (
<div>
<div className="field">
<FieldLabel>{label}</FieldLabel> <FieldLabel>{label}</FieldLabel>
<progress value={progress} max="100">{progress}%</progress>
<Progress value={progress} />
</div> </div>
) )
} }

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

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

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

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

31
src/components/group-invitations.tsx

@ -1,10 +1,10 @@
import React, { FC, useEffect } from 'react' import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import { Link, useHistory } from 'react-router-dom' import { Link, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheckCircle, faStopwatch, faPauseCircle } from '@fortawesome/free-solid-svg-icons' import { faCheckCircle, faStopwatch, faPauseCircle } from '@fortawesome/free-solid-svg-icons'
import moment from 'moment' import moment from 'moment'
import { useTheme } from 'src/hooks'
import { handleApiError } from 'src/api/errors' import { handleApiError } from 'src/api/errors'
import { fetchInvitations, createInvitation } from 'src/actions/groups' import { fetchInvitations, createInvitation } from 'src/actions/groups'
import { getInvitations } from 'src/selectors/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 { 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 Subtitle from 'src/components/subtitle'
import SelectField from 'src/components/controls/select-field' import SelectField from 'src/components/controls/select-field'
@ -21,6 +21,7 @@ interface Props {
} }
const GroupInvitations: FC<Props> = ({ group }) => { const GroupInvitations: FC<Props> = ({ group }) => {
const theme = useTheme()
const invitations = useSelector<AppState, Invitation[]>(getInvitations) const invitations = useSelector<AppState, Invitation[]>(getInvitations)
const expiration = useSelector<AppState, string>(state => getFieldValue<string>(state, 'expiration', '0')) const expiration = useSelector<AppState, string>(state => getFieldValue<string>(state, 'expiration', '0'))
const limit = useSelector<AppState, string>(state => getFieldValue<string>(state, 'limit', '0')) const limit = useSelector<AppState, string>(state => getFieldValue<string>(state, 'limit', '0'))
@ -63,29 +64,17 @@ const GroupInvitations: FC<Props> = ({ group }) => {
return ( return (
<div> <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 /> <br />
{invitations.length > 0 && {invitations.length > 0 &&
<table className="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
<table>
<thead> <thead>
<tr> <tr>
<th>Code</th> <th>Code</th>

10
src/components/group-logs.tsx

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

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

@ -21,12 +21,12 @@ const MemberListItem: FC<Props> = ({ member }) => {
} }
return ( 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 /> <br />
<Link to={`/u/${member.id}`} style={{ color: theme.secondary, fontSize: '0.9rem' }}>@{member.id}</Link> <Link to={`/u/${member.id}`} style={{ color: theme.secondary, fontSize: '0.9rem' }}>@{member.id}</Link>
<br /> <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> </div>
) )
} }

2
src/components/member-list.tsx

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

21
src/components/pages/about.tsx

@ -1,10 +1,15 @@
import React, { FC, useEffect } from 'react' import React, { FC, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useTheme } from 'src/hooks'
import { setTitle } from 'src/utils' import { setTitle } from 'src/utils'
import Section from 'src/components/section' import Section from 'src/components/section'
import Title from 'src/components/title' import Title from 'src/components/title'
import Subtitle from 'src/components/subtitle'
const About: FC = () => { const About: FC = () => {
const theme = useTheme()
useEffect(() => { useEffect(() => {
setTitle('About Flexor', false) setTitle('About Flexor', false)
}) })
@ -14,9 +19,19 @@ const About: FC = () => {
<Section> <Section>
<Title>About Flexor</Title> <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> </Section>
</div> </div>
) )

30
src/components/pages/developers.tsx

@ -31,13 +31,31 @@ const Developers: FC = () => {
<Subtitle>Developer Documentation</Subtitle> <Subtitle>Developer Documentation</Subtitle>
<HorizontalRule /> <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')} /> <PrimaryButton text="Create a new App" icon={faPlusCircle} onClick={() => history.push('/developers/create')} />
</Section> </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 { useSelector, useDispatch } from 'react-redux'
import { Link, useParams, useHistory } from 'react-router-dom' import { Link, useParams, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 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 { handleApiError } from 'src/api/errors'
import { initForm, initField } from 'src/actions/forms' 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 { getEntity } from 'src/selectors/entities'
import { getForm } from 'src/selectors/forms' import { getForm } from 'src/selectors/forms'
import { useDeepCompareEffect } from 'src/hooks'
import { useDeepCompareEffect, useTheme } from 'src/hooks'
import { setTitle, valueFromForm } from 'src/utils' import { setTitle, valueFromForm } from 'src/utils'
import { import {
AppState, AppState,
@ -24,7 +24,9 @@ import {
import Title from 'src/components/title' import Title from 'src/components/title'
import Subtitle from 'src/components/subtitle' import Subtitle from 'src/components/subtitle'
import Section from 'src/components/section'
import HorizontalRule from 'src/components/horizontal-rule' import HorizontalRule from 'src/components/horizontal-rule'
import PrimaryButton from 'src/components/controls/primary-button'
import MemberList from 'src/components/member-list' import MemberList from 'src/components/member-list'
import GroupInvitations from 'src/components/group-invitations' import GroupInvitations from 'src/components/group-invitations'
import GroupLogs from 'src/components/group-logs' import GroupLogs from 'src/components/group-logs'
@ -48,6 +50,7 @@ const tabs: Tab[] = [
] ]
const GroupAdmin: FC = () => { const GroupAdmin: FC = () => {
const theme = useTheme()
const { id, tab = '' } = useParams<Params>() const { id, tab = '' } = useParams<Params>()
const history = useHistory() const history = useHistory()
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id)) const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
@ -101,58 +104,60 @@ const GroupAdmin: FC = () => {
return ( return (
<div> <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>
<div className="container">
<div>
{tab === '' && {tab === '' &&
<div> <div>
<div className="field"> <div className="field">
<FieldLabel>ID</FieldLabel> <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>
</div> </div>
<br />
<div className="field"> <div className="field">
<FieldLabel>Name</FieldLabel> <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>
</div> </div>
<br />
<TextareaField name="about" label="About" placeholder="About this Community" /> <TextareaField name="about" label="About" placeholder="About this Community" />
<br />
<ImageField name="image" /> <ImageField name="image" />
<br />
<CoverImageField name="coverImage" /> <CoverImageField name="coverImage" />
<br />
<IconImageField name="iconImage" /> <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> </div>
} }
@ -161,14 +166,18 @@ const GroupAdmin: FC = () => {
<GroupInvitations group={id} /> <GroupInvitations group={id} />
<HorizontalRule /> <HorizontalRule />
<Title>Members</Title>
<Subtitle>Members</Subtitle>
<MemberList group={id} /> <MemberList group={id} />
</div> </div>
} }
{tab === 'logs' && <GroupLogs group={id} />}
{tab === 'logs' &&
<div>
<GroupLogs group={id} />
</div>
}
</div> </div>
</div>
</Section>
</div> </div>
) )
} }

4
src/components/pages/groups.tsx

@ -29,6 +29,10 @@ const Groups: FC = () => {
<Section> <Section>
<Title>Communities</Title> <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} />)} {groups.map(group => <GroupListItem group={group} />)}
<HorizontalRule /> <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 { getEntity } from 'src/selectors/entities'
import { getForm } from 'src/selectors/forms' 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 { useDeepCompareEffect } from 'src/hooks'
import { AppState, AppThunkDispatch, Group, EntityType, NotificationType, Form } from 'src/types' 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-name', '', 'name'))
dispatch(initField('user-email', '', 'email')) dispatch(initField('user-email', '', 'email'))
dispatch(initField('password', '')) dispatch(initField('password', ''))
dispatch(initField('user-theme', '', 'theme'))
dispatch(initField('user-theme', getDefaultThemeName(), 'theme'))
dispatch(initField('user-image', '', 'imageUrl')) dispatch(initField('user-image', '', 'imageUrl'))
dispatch(initField('user-cover-image', '', 'coverImageUrl')) dispatch(initField('user-cover-image', '', 'coverImageUrl'))
dispatch(initField('user-agree', false)) dispatch(initField('user-agree', false))
@ -75,7 +75,7 @@ const RegisterGroup: FC = () => {
requiresApproval: valueFromForm<boolean>(form, 'user-requires-approval', false), requiresApproval: valueFromForm<boolean>(form, 'user-requires-approval', false),
privacy: valueFromForm<string>(form, 'user-privacy', 'public'), privacy: valueFromForm<string>(form, 'user-privacy', 'public'),
group: id, group: id,
theme: valueFromForm<string>(form, 'user-theme', ''),
theme: valueFromForm<string>(form, 'user-theme', getDefaultThemeName()),
})) }))
dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`)) 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 { initForm, initField } from 'src/actions/forms'
import { showNotification } from 'src/actions/notifications' import { showNotification } from 'src/actions/notifications'
import { createGroup, register } from 'src/actions/registration' 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 { AppState, AppThunkDispatch, Form, NotificationType } from 'src/types'
import Title from 'src/components/title' import Title from 'src/components/title'
@ -43,7 +43,7 @@ const Register: FC = () => {
coverImageUrl: valueFromForm<string>(form, 'user-cover-image', ''), coverImageUrl: valueFromForm<string>(form, 'user-cover-image', ''),
requiresApproval: valueFromForm<boolean>(form, 'user-requires-approval', false), requiresApproval: valueFromForm<boolean>(form, 'user-requires-approval', false),
privacy: valueFromForm<string>(form, 'user-privacy', 'open'), privacy: valueFromForm<string>(form, 'user-privacy', 'open'),
theme: valueFromForm<string>(form, 'user-theme', ''),
theme: valueFromForm<string>(form, 'user-theme', getDefaultThemeName()),
})) }))
await dispatch(createGroup({ await dispatch(createGroup({
@ -52,7 +52,7 @@ const Register: FC = () => {
imageUrl: valueFromForm<string>(form, 'group-image', ''), imageUrl: valueFromForm<string>(form, 'group-image', ''),
coverImageUrl: valueFromForm<string>(form, 'group-cover-image', ''), coverImageUrl: valueFromForm<string>(form, 'group-cover-image', ''),
iconImageUrl: valueFromForm<string>(form, 'group-icon-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!`)) dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`))
@ -84,7 +84,7 @@ const Register: FC = () => {
dispatch(initField('group-image', '', 'imageUrl')) dispatch(initField('group-image', '', 'imageUrl'))
dispatch(initField('group-cover-image', '', 'coverImageUrl')) dispatch(initField('group-cover-image', '', 'coverImageUrl'))
dispatch(initField('group-icon-image', '', 'iconImageUrl')) dispatch(initField('group-icon-image', '', 'iconImageUrl'))
dispatch(initField('group-theme', '', 'theme'))
dispatch(initField('group-theme', getDefaultThemeName(), 'theme'))
dispatch(initField('group-agree', false)) dispatch(initField('group-agree', false))
dispatch(initField('user-id', '', 'id')) dispatch(initField('user-id', '', 'id'))
dispatch(initField('user-name', '', 'name')) dispatch(initField('user-name', '', 'name'))
@ -94,7 +94,7 @@ const Register: FC = () => {
dispatch(initField('user-cover-image', '', 'coverImageUrl')) dispatch(initField('user-cover-image', '', 'coverImageUrl'))
dispatch(initField('user-requires-approval', false, 'requiresApproval')) dispatch(initField('user-requires-approval', false, 'requiresApproval'))
dispatch(initField('user-privacy', 'public', 'privacy')) dispatch(initField('user-privacy', 'public', 'privacy'))
dispatch(initField('user-theme', '', 'theme'))
dispatch(initField('user-theme', getDefaultThemeName(), 'theme'))
dispatch(initField('user-agree', false)) 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" /> <TextField name="name" label="Name" placeholder="Your Display Name" />
<TextareaField name="about" label="About" placeholder="Your Bio" /> <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} /> <SelectField name="privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<ImageField name="image" label="Avatar" /> <ImageField name="image" label="Avatar" />
<CoverImageField name="coverImage" /> <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 { useSelector, useDispatch } from 'react-redux'
import { useParams, useHistory } from 'react-router-dom' import { useParams, useHistory } from 'react-router-dom'
import { faEdit, faUserCheck, faBan } from '@fortawesome/free-solid-svg-icons' 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 { fetchGroup } from 'src/actions/groups'
import { getAuthenticated } from 'src/selectors/authentication' import { getAuthenticated } from 'src/selectors/authentication'
import { getEntity } from 'src/selectors/entities' import { getEntity } from 'src/selectors/entities'
import { getThemeName } from 'src/selectors/theme'
import { useDeepCompareEffect, useConfig, useTheme } from 'src/hooks' import { useDeepCompareEffect, useConfig, useTheme } from 'src/hooks'
import { setTitle, urlForBlob } from 'src/utils' 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 Button from 'src/components/controls/button'
import Loading from 'src/components/pages/loading' import Loading from 'src/components/pages/loading'
import HorizontalRule from 'src/components/horizontal-rule' import HorizontalRule from 'src/components/horizontal-rule'
import { setTheme } from 'src/actions/theme'
interface Params { interface Params {
id: string id: string
@ -28,6 +30,8 @@ interface Params {
const ViewGroup: FC = () => { const ViewGroup: FC = () => {
const { id } = useParams<Params>() const { id } = useParams<Params>()
const theme = useTheme() 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 group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
const authenticated = useSelector<AppState, boolean>(getAuthenticated) const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const dispatch = useDispatch<AppThunkDispatch>() const dispatch = useDispatch<AppThunkDispatch>()
@ -43,7 +47,14 @@ const ViewGroup: FC = () => {
}, []) }, [])
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
if (group) setTitle(group.name)
if (group) {
setTitle(group.name)
dispatch(setTheme(group.theme))
}
return () => {
dispatch(setTheme(selectedThemeName))
}
}, [group]) }, [group])
if (!group) return <Loading /> 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 { useSelector, useDispatch } from 'react-redux'
import { useParams, useHistory } from 'react-router-dom' import { useParams, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@ -8,13 +8,15 @@ import moment from 'moment'
import { handleApiError } from 'src/api/errors' import { handleApiError } from 'src/api/errors'
import { fetchUser, subscribe, unsubscribe } from 'src/actions/users' import { fetchUser, subscribe, unsubscribe } from 'src/actions/users'
import { fetchUserPosts } from 'src/actions/posts' import { fetchUserPosts } from 'src/actions/posts'
import { setTheme } from 'src/actions/theme'
import { getEntity } from 'src/selectors/entities' import { getEntity } from 'src/selectors/entities'
import { getAuthenticatedUser, getChecked } from 'src/selectors/authentication' import { getAuthenticatedUser, getChecked } from 'src/selectors/authentication'
import { getUserPosts } from 'src/selectors/posts' import { getUserPosts } from 'src/selectors/posts'
import { getThemeName } from 'src/selectors/theme'
import { useDeepCompareEffect, useConfig, useTheme } from 'src/hooks' import { useDeepCompareEffect, useConfig, useTheme } from 'src/hooks'
import { setTitle, urlForBlob } from 'src/utils' 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 Title from 'src/components/title'
import Subtitle from 'src/components/subtitle' import Subtitle from 'src/components/subtitle'
@ -31,6 +33,8 @@ interface Params {
const ViewUser: FC = () => { const ViewUser: FC = () => {
const { id } = useParams<Params>() const { id } = useParams<Params>()
const theme = useTheme() const theme = useTheme()
const themeName = useSelector<AppState, string>(getThemeName)
const [selectedThemeName] = useState(themeName)
const checked = useSelector<AppState, boolean>(getChecked) const checked = useSelector<AppState, boolean>(getChecked)
const self = useSelector<AppState, User | undefined>(getAuthenticatedUser) const self = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const user = useSelector<AppState, User | undefined>(state => getEntity<User>(state, EntityType.User, id)) const user = useSelector<AppState, User | undefined>(state => getEntity<User>(state, EntityType.User, id))
@ -53,7 +57,14 @@ const ViewUser: FC = () => {
}, [checked]) }, [checked])
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
if (user) setTitle(user.name)
if (user) {
setTitle(user.name)
dispatch(setTheme(user.theme))
}
return () => {
dispatch(setTheme(selectedThemeName))
}
}, [user]) }, [user])
if (!user) return <Loading /> 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 Section: FC = ({ children }) => {
const theme = useTheme() const theme = useTheme()
return ( return (
<section style={{ backgroundColor: theme.backgroundPrimary }}>
<section style={{ backgroundColor: theme.backgroundPrimary, color: theme.text }}>
{children} {children}
</section> </section>
) )

4
src/components/self-info.tsx

@ -31,12 +31,12 @@ const SelfInfo: FC = () => {
if (user.name) { if (user.name) {
return ( return (
<Link to="/self" style={{ color: theme.primaryAlternate }}> <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> </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 ( return (

3
src/reducers/theme.ts

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

51
src/styles/app.css

@ -34,7 +34,7 @@ input, textarea, select {
border-radius: 0; border-radius: 0;
box-sizing: border-box; box-sizing: border-box;
font-family: var(--default-font); font-family: var(--default-font);
font-size: 1rem;
font-size: 0.9rem;
margin: 0px; margin: 0px;
padding: var(--input-padding); padding: var(--input-padding);
width: 100%; width: 100%;
@ -175,6 +175,54 @@ footer {
text-align: center; 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 { div.field {
margin: 2rem 0px; margin: 2rem 0px;
} }
@ -266,7 +314,6 @@ p.label + p.content {
} }
div.member { div.member {
border: var(--default-border);
margin-right: 10px; margin-right: 10px;
min-width: 150px; min-width: 150px;
padding: 1rem; padding: 1rem;

54
src/themes.ts

@ -1,7 +1,7 @@
import { ThemeCollection, ColorScheme } from 'src/types' import { ThemeCollection, ColorScheme } from 'src/types'
const themes: ThemeCollection = { const themes: ThemeCollection = {
'blue': {
'blueish': {
[ColorScheme.Light]: { [ColorScheme.Light]: {
primary: '#3b42f4', primary: '#3b42f4',
primaryAlternate: '#fff', primaryAlternate: '#fff',
@ -25,7 +25,7 @@ const themes: ThemeCollection = {
blue: '#005ce6', blue: '#005ce6',
}, },
}, },
'orange': {
'orangeish': {
[ColorScheme.Light]: { [ColorScheme.Light]: {
primary: '#ff8000', primary: '#ff8000',
primaryAlternate: '#fff', primaryAlternate: '#fff',
@ -49,7 +49,7 @@ const themes: ThemeCollection = {
blue: '#005ce6', blue: '#005ce6',
}, },
}, },
'green': {
'greenish': {
[ColorScheme.Light]: { [ColorScheme.Light]: {
primary: '#004d00', primary: '#004d00',
primaryAlternate: '#fff', primaryAlternate: '#fff',
@ -73,6 +73,54 @@ const themes: ThemeCollection = {
blue: '#005ce6', 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 export default themes

2
src/types/entities.ts

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

3
src/utils/index.ts

@ -1,4 +1,5 @@
import getConfig from 'src/config' import getConfig from 'src/config'
import themes from 'src/themes'
import { import {
NotificationType, 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 classNames = (dictionary: ClassDictionary) => Object.entries(dictionary).filter(([_, value]) => !!value).map(([key, _]) => key).join(' ')
export const getDefaultThemeName = () => Object.keys(themes)[0]
Loading…
Cancel
Save