From a494d5ca14496cbc3f59c39d9caf18ff566a416f Mon Sep 17 00:00:00 2001 From: Dwayne Harris Date: Sun, 17 Nov 2019 16:19:46 -0500 Subject: [PATCH] WIP --- package-lock.json | 100 ++++++------------------ package.json | 11 ++- src/components/app.tsx | 11 ++- src/components/controls/file-field.tsx | 43 +++++----- src/components/controls/theme-field.tsx | 19 ++--- src/components/create-user-step.tsx | 4 +- src/components/group-invitations.tsx | 31 +++----- src/components/group-logs.tsx | 10 ++- src/components/member-list-item.tsx | 6 +- src/components/member-list.tsx | 2 +- src/components/pages/about.tsx | 21 ++++- src/components/pages/developers.tsx | 30 +++++-- src/components/pages/group-admin.tsx | 87 ++++++++++++--------- src/components/pages/groups.tsx | 4 + src/components/pages/register-group.tsx | 6 +- src/components/pages/register.tsx | 10 +-- src/components/pages/self.tsx | 2 +- src/components/pages/view-group.tsx | 15 +++- src/components/pages/view-user.tsx | 17 +++- src/components/progress.tsx | 17 ++++ src/components/section.tsx | 2 +- src/components/self-info.tsx | 4 +- src/reducers/theme.ts | 3 +- src/styles/app.css | 51 +++++++++++- src/themes.ts | 54 ++++++++++++- src/types/entities.ts | 2 + src/utils/index.ts | 3 + 27 files changed, 347 insertions(+), 218 deletions(-) create mode 100644 src/components/progress.tsx diff --git a/package-lock.json b/package-lock.json index be80122..c44a665 100644 --- a/package-lock.json +++ b/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", diff --git a/package.json b/package.json index 2548d25..c859bf3 100644 --- a/package.json +++ b/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", diff --git a/src/components/app.tsx b/src/components/app.tsx index 2827147..e73c87d 100644 --- a/src/components/app.tsx +++ b/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(getAuthenticatedUser) const fetching = useSelector(getFetching) const dispatch = useDispatch() @@ -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 = () => { -
+
diff --git a/src/components/controls/file-field.tsx b/src/components/controls/file-field.tsx index 64b6517..02c5cc8 100644 --- a/src/components/controls/file-field.tsx +++ b/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 => { const theme = useTheme() const value = useSelector(state => getFieldValue(state, props.name, false)) const config = useConfig() - const dispatch = useDispatch() + const dispatch = useDispatch() const { name, label, help, previewWidth = 128, maxSize = config.media.defaultMaxSize } = props @@ -50,33 +50,32 @@ const FileField: FC = 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 => { if (uploading) { return ( -
+
{label} - {progress}% +
) } diff --git a/src/components/controls/theme-field.tsx b/src/components/controls/theme-field.tsx index 2b87bb9..8c86a37 100644 --- a/src/components/controls/theme-field.tsx +++ b/src/components/controls/theme-field.tsx @@ -17,10 +17,9 @@ export interface Props { } const ThemeField: FC = ({ name, label }) => { - const currentTheme = useTheme() - const currentThemeName = useSelector(getThemeName) - const [previousThemeName, setPreviousThemeName] = useState('') + const theme = useTheme() const value = useSelector(state => getFieldValue(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 = ({ 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 (
{label}
- {themeList.map(([themeName, theme]) => ( + {themeList.map(([themeName, t]) => (
handleMouseEnter(themeName)} onMouseLeave={() => handleMouseLeave()} onClick={() => dispatch(setFieldValue(name, themeName))}>
))}
-
{capitalize(currentThemeName)}
+
{capitalize(displayedName)}
) diff --git a/src/components/create-user-step.tsx b/src/components/create-user-step.tsx index f713705..eefa29b 100644 --- a/src/components/create-user-step.tsx +++ b/src/components/create-user-step.tsx @@ -75,9 +75,9 @@ const CreateUserStep: FC = () => { -
diff --git a/src/components/group-invitations.tsx b/src/components/group-invitations.tsx index d56e1f1..aef8c2c 100644 --- a/src/components/group-invitations.tsx +++ b/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 = ({ group }) => { + const theme = useTheme() const invitations = useSelector(getInvitations) const expiration = useSelector(state => getFieldValue(state, 'expiration', '0')) const limit = useSelector(state => getFieldValue(state, 'limit', '0')) @@ -63,29 +64,17 @@ const GroupInvitations: FC = ({ group }) => { return (
- Invitations - Create an invitation for someone to create a new account in this Community. + Invitations +

Create an invitation for someone to create a new account in this Community.

-
- - - -
-
 
-
- -
-
-
+ + + + handleCreateInvitation()} />
{invitations.length > 0 && - +
diff --git a/src/components/group-logs.tsx b/src/components/group-logs.tsx index a310438..b88bca3 100644 --- a/src/components/group-logs.tsx +++ b/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 = ({ group }) => { + const theme = useTheme() const logs = useSelector(getLogs) const dispatch = useDispatch() @@ -28,8 +30,8 @@ const MemberList: FC = ({ group }) => { return (
Code
- - + + @@ -37,8 +39,8 @@ const MemberList: FC = ({ group }) => { {logs.map(log => ( - - + + diff --git a/src/components/member-list-item.tsx b/src/components/member-list-item.tsx index 71fa712..08cc8e7 100644 --- a/src/components/member-list-item.tsx +++ b/src/components/member-list-item.tsx @@ -21,12 +21,12 @@ const MemberListItem: FC = ({ member }) => { } return ( -
- {member.name} +
+ {member.name}
@{member.id}
- {capitalize(member.membership as string)} + {capitalize(member.membership as string)}
) } diff --git a/src/components/member-list.tsx b/src/components/member-list.tsx index bc1ac96..a6eda82 100644 --- a/src/components/member-list.tsx +++ b/src/components/member-list.tsx @@ -27,7 +27,7 @@ const MemberList: FC = ({ group }) => { }, [group]) return ( -
+
{members.map(member => )}
) diff --git a/src/components/pages/about.tsx b/src/components/pages/about.tsx index 9d465c8..2723c80 100644 --- a/src/components/pages/about.tsx +++ b/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 = () => {
About Flexor -

- Flexor is a website. -

+

Flexor is a service that lets users post stuff for their subscribers to see. Here are some things to know about how it works:

+ + Communities + +

Flexor is made up of Communities. Each account is created through one. Communities enforce their own standards of behavior.

+ +

Check out the list of Communities.

+ + Apps + +

Users post content to Flexor through apps created by other people/organizations.

+ +

Check out the list of Apps.

) diff --git a/src/components/pages/developers.tsx b/src/components/pages/developers.tsx index 0546df2..1137f2b 100644 --- a/src/components/pages/developers.tsx +++ b/src/components/pages/developers.tsx @@ -31,13 +31,31 @@ const Developers: FC = () => { Developer Documentation -

Flexor apps allow users to express themselves on the network.

-
-

Developer documentation coming soon.

- +

Flexor Apps let Users post stuff to the service.

+

Each App has two parts:

+ + Composer + +

The Composer is the interface for creating a post. It can be anything that results in a post object being made.

+ +

+ The composerURL 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 postMessage messages. +

-

This is where you manage apps you create.

-
+ Renderer + +

+ 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 rendererURL field is empty. +

+ +

+ The rendererURL field of an app should point to an HTML page that will be rendered in an iFrame in the Flexor app. +

+ + history.push('/developers/create')} /> diff --git a/src/components/pages/group-admin.tsx b/src/components/pages/group-admin.tsx index 6dcc9d2..cf5e745 100644 --- a/src/components/pages/group-admin.tsx +++ b/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() const history = useHistory() const group = useSelector(state => getEntity(state, EntityType.Group, id)) @@ -101,58 +104,60 @@ const GroupAdmin: FC = () => { return (
- {group.name} - Administration - - -
-
-
    - {tabs.map(t => ( -
  • - - {t.label} - -
  • - ))} -
+
+ {group.name} + Administration + + +
+ {tabs.map(t => ( +
+ + {t.label} + +
+ ))}
-
+
{tab === '' &&
ID -
- +
+
+ +
+
+ +
-
Name -
- +
+
+ +
-
-
- -
-
-

- - +
+ + handleUpdateGroup()} />
} @@ -161,14 +166,18 @@ const GroupAdmin: FC = () => { - Members + Members
} - {tab === 'logs' && } + {tab === 'logs' && +
+ +
+ }
-
+
) } diff --git a/src/components/pages/groups.tsx b/src/components/pages/groups.tsx index 8708fdc..fe3c6f4 100644 --- a/src/components/pages/groups.tsx +++ b/src/components/pages/groups.tsx @@ -29,6 +29,10 @@ const Groups: FC = () => {
Communities +

Flexor is made up of Communities. Each User account is created through one.

+ + + {groups.map(group => )} diff --git a/src/components/pages/register-group.tsx b/src/components/pages/register-group.tsx index 8d94086..185d803 100644 --- a/src/components/pages/register-group.tsx +++ b/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(form, 'user-requires-approval', false), privacy: valueFromForm(form, 'user-privacy', 'public'), group: id, - theme: valueFromForm(form, 'user-theme', ''), + theme: valueFromForm(form, 'user-theme', getDefaultThemeName()), })) dispatch(showNotification(NotificationType.Welcome, `Welcome to Flexor!`)) diff --git a/src/components/pages/register.tsx b/src/components/pages/register.tsx index aa5cfd6..c996a94 100644 --- a/src/components/pages/register.tsx +++ b/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(form, 'user-cover-image', ''), requiresApproval: valueFromForm(form, 'user-requires-approval', false), privacy: valueFromForm(form, 'user-privacy', 'open'), - theme: valueFromForm(form, 'user-theme', ''), + theme: valueFromForm(form, 'user-theme', getDefaultThemeName()), })) await dispatch(createGroup({ @@ -52,7 +52,7 @@ const Register: FC = () => { imageUrl: valueFromForm(form, 'group-image', ''), coverImageUrl: valueFromForm(form, 'group-cover-image', ''), iconImageUrl: valueFromForm(form, 'group-icon-image', ''), - theme: valueFromForm(form, 'group-theme', ''), + theme: valueFromForm(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)) }, []) diff --git a/src/components/pages/self.tsx b/src/components/pages/self.tsx index 4c965c8..7207d6b 100644 --- a/src/components/pages/self.tsx +++ b/src/components/pages/self.tsx @@ -132,7 +132,7 @@ const Self: FC = () => { - + diff --git a/src/components/pages/view-group.tsx b/src/components/pages/view-group.tsx index 53210c2..9710acd 100644 --- a/src/components/pages/view-group.tsx +++ b/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() const theme = useTheme() + const themeName = useSelector(getThemeName) + const [selectedThemeName] = useState(themeName) const group = useSelector(state => getEntity(state, EntityType.Group, id)) const authenticated = useSelector(getAuthenticated) const dispatch = useDispatch() @@ -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 diff --git a/src/components/pages/view-user.tsx b/src/components/pages/view-user.tsx index ab2e5ce..34b8c21 100644 --- a/src/components/pages/view-user.tsx +++ b/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() const theme = useTheme() + const themeName = useSelector(getThemeName) + const [selectedThemeName] = useState(themeName) const checked = useSelector(getChecked) const self = useSelector(getAuthenticatedUser) const user = useSelector(state => getEntity(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 diff --git a/src/components/progress.tsx b/src/components/progress.tsx new file mode 100644 index 0000000..feadab6 --- /dev/null +++ b/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 = ({ value }) => { + const theme = useTheme() + return ( +
+
+
+ ) +} + +export default Progress diff --git a/src/components/section.tsx b/src/components/section.tsx index e0e6ba5..cabf37f 100644 --- a/src/components/section.tsx +++ b/src/components/section.tsx @@ -4,7 +4,7 @@ import { useTheme } from 'src/hooks' const Section: FC = ({ children }) => { const theme = useTheme() return ( -
+
{children}
) diff --git a/src/components/self-info.tsx b/src/components/self-info.tsx index f6d30ac..764212b 100644 --- a/src/components/self-info.tsx +++ b/src/components/self-info.tsx @@ -31,12 +31,12 @@ const SelfInfo: FC = () => { if (user.name) { return ( - {user.name} @{user.id} + {user.name} @{user.id} ) } - return @{user.id} + return @{user.id} } return ( diff --git a/src/reducers/theme.ts b/src/reducers/theme.ts index b9df442..d0133d9 100644 --- a/src/reducers/theme.ts +++ b/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 = (state = initialState, action) => { diff --git a/src/styles/app.css b/src/styles/app.css index 81b1122..8482e41 100644 --- a/src/styles/app.css +++ b/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; diff --git a/src/themes.ts b/src/themes.ts index ed58bc1..8c2b94f 100644 --- a/src/themes.ts +++ b/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 diff --git a/src/types/entities.ts b/src/types/entities.ts index 3d9e171..861b32e 100644 --- a/src/types/entities.ts +++ b/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[] diff --git a/src/utils/index.ts b/src/utils/index.ts index d90c773..225fec7 100644 --- a/src/utils/index.ts +++ b/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]
Who What When
{log.user.id}
{log.user.id} {log.content} {moment(log.created).format('MMMM Do YYYY, h:mm:ss a')}