diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index d9b5d1230..ee1c34dfe 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -1,6 +1,7 @@ import 'antd/dist/antd.css'; import '../styles/colors.scss'; import '../styles/globals.scss'; +import '../styles/ant-overrides.scss'; import '../styles/home.scss'; import '../styles/chat.scss'; diff --git a/web/pages/components/config/README.md b/web/pages/components/config/README.md index b8ec0f451..eae4c4bfc 100644 --- a/web/pages/components/config/README.md +++ b/web/pages/components/config/README.md @@ -1,5 +1,49 @@ -# Config +# About the Config editing section +An adventure with React, React Hooks and Ant Design forms. + +## General data flow in this React app + +### First things to note +- When the Admin app loads, the `ServerStatusContext` (in addition to checking server `/status` on a timer) makes a call to the `/serverconfig` API to get your config details. This data will be stored as **`serverConfig`** in app state, and _provided_ to the app via `useContext` hook. + +- The `serverConfig` in state is be the central source of data that pre-populates the forms. + +- The `ServerStatusContext` also provides a method for components to update the serverConfig state, called `setFieldInConfigState()`. + +- After you have updated a config value in a form field, and successfully submitted it through its endpoint, you should call `setFieldInConfigState` to update the global state with the new value. + +- Each top field of the serverConfig has its own API update endpoint. + +### Form Flow +Each form input (or group of inputs) you make, you should + 1. Get the field values that you want out of `serverConfig` from ServerStatusContext with `useContext`. + 2. Next we'll have to put these field values of interest into a `useState` in each grouping. This will help you edit the form. + 3. Because ths config data is populated asynchronously, Use a `useEffect` to check when that data has arrived before putting it into state. + 4. You will be using the state's value to populate the `defaultValue` and the `value` props of each Ant input component (`Input`, `Toggle`, `Switch`, `Select`, `Slider` are currently used). + 5. When an `onChange` event fires for each type of input component, you will update the local state of each page with the changed value. + 6. Depending on the form, an `onChange` of the input component, or a subsequent `onClick` of a submit button will take the value from local state and POST the field's API. + 7. `onSuccess` of the post, you should update the global app state with the new value. + +There are also a variety of other local states to manage the display of error/success messaging. + +## Using Ant's `
` with `form-textfield`. +You may see that a couple of pages (currently Public Details and Server Details page), is mainly a grouping of similar Text fields. + +`const [form] = Form.useForm();` +`form.setFieldsValue(initialValues);` + + +A special `TextField` component was created to be used with form. + + +## Potential Optimizations + +Looking back at the pages with `` + `form-textfield`, t + +This pattern might be overly engineered. + +There are also a few patterns across all the form groups that repeat quite a bit. Perhaps these patterns could be consolidated into a custom hook that could handle all the steps. TODO: explain how to use and how the custom `form-xxxx` components work together. @@ -48,4 +92,12 @@ TODO: - page headers - diff color? - fix social handles icon in table - consolidate things into 1 page? - - things could use smaller font? \ No newline at end of file + - things could use smaller font? +- maybe convert common form pattern to custom hook? + + +Possibly over engineered + +https://uxcandy.co/demo/label_pro/preview/demo_2/pages/forms/form-elements.html + +https://www.bootstrapdash.com/demo/corona/jquery/template/modern-vertical/pages/forms/basic_elements.html \ No newline at end of file diff --git a/web/pages/config-social-links.tsx b/web/pages/components/config/edit-social-links.tsx similarity index 94% rename from web/pages/config-social-links.tsx rename to web/pages/components/config/edit-social-links.tsx index 7788b3443..50b689e27 100644 --- a/web/pages/config-social-links.tsx +++ b/web/pages/components/config/edit-social-links.tsx @@ -2,16 +2,18 @@ import React, { useState, useContext, useEffect } from 'react'; import { Typography, Table, Button, Modal, Input } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { DeleteOutlined } from '@ant-design/icons'; -import SocialDropdown from './components/config/social-icons-dropdown'; -import { fetchData, NEXT_PUBLIC_API_HOST, SOCIAL_PLATFORMS_LIST } from '../utils/apis'; -import { ServerStatusContext } from '../utils/server-status-context'; -import { API_SOCIAL_HANDLES, postConfigUpdateToAPI, RESET_TIMEOUT, SUCCESS_STATES, DEFAULT_SOCIAL_HANDLE, OTHER_SOCIAL_HANDLE_OPTION } from './components/config/constants'; -import { SocialHandle } from '../types/config-section'; -import {isValidUrl} from '../utils/urls'; +import SocialDropdown from './social-icons-dropdown'; +import { fetchData, NEXT_PUBLIC_API_HOST, SOCIAL_PLATFORMS_LIST } from '../../../utils/apis'; +import { ServerStatusContext } from '../../../utils/server-status-context'; +import { API_SOCIAL_HANDLES, postConfigUpdateToAPI, RESET_TIMEOUT, SUCCESS_STATES, DEFAULT_SOCIAL_HANDLE, OTHER_SOCIAL_HANDLE_OPTION } from './constants'; +import { SocialHandle } from '../../../types/config-section'; +import {isValidUrl} from '../../../utils/urls'; + +import configStyles from '../../../styles/config-pages.module.scss'; const { Title } = Typography; -export default function ConfigSocialLinks() { +export default function EditSocialLinks() { const [availableIconsList, setAvailableIconsList] = useState([]); const [currentSocialHandles, setCurrentSocialHandles] = useState([]); @@ -228,14 +230,14 @@ export default function ConfigSocialLinks() { return ( -
+
Social Links

Add all your social media handles and links to your other profiles here.

{statusMessage} record.url} diff --git a/web/pages/components/config/form-textfield.tsx b/web/pages/components/config/form-textfield.tsx index 8db66d1ae..1cf5c3c81 100644 --- a/web/pages/components/config/form-textfield.tsx +++ b/web/pages/components/config/form-textfield.tsx @@ -155,10 +155,9 @@ export default function TextField(props: TextFieldProps) { return (
+
{label}
- -
+ { hasChanged ? : null } diff --git a/web/pages/components/main-layout.tsx b/web/pages/components/main-layout.tsx index 44153ef7e..23b2ccbe4 100644 --- a/web/pages/components/main-layout.tsx +++ b/web/pages/components/main-layout.tsx @@ -141,12 +141,14 @@ export default function MainLayout(props) { Public Details + + Social items + + Custom page content - - Social links - + Server Details diff --git a/web/pages/config-public-details.tsx b/web/pages/config-public-details.tsx index 28f257608..4778f95e3 100644 --- a/web/pages/config-public-details.tsx +++ b/web/pages/config-public-details.tsx @@ -4,12 +4,11 @@ import Link from 'next/link'; import TextField, { TEXTFIELD_TYPE_TEXTAREA, TEXTFIELD_TYPE_URL } from './components/config/form-textfield'; -import EditInstanceTags from './components/config/edit-tags'; -import EditDirectoryDetails from './components/config/edit-directory'; - import { ServerStatusContext } from '../utils/server-status-context'; import { TEXTFIELD_DEFAULTS, postConfigUpdateToAPI } from './components/config/constants'; +import configStyles from '../styles/config-pages.module.scss'; + const { Title } = Typography; export default function PublicFacingDetails() { @@ -29,11 +28,6 @@ export default function PublicFacingDetails() { form.setFieldsValue(initialValues); }, [instanceDetails]); - // const handleResetValue = (fieldName: string) => { - // const defaultValue = defaultFields[fieldName] && defaultFields[fieldName].defaultValue || ''; - - // form.setFieldsValue({ [fieldName]: initialValues[fieldName] || defaultValue }); - // } // if instanceUrl is empty, we should also turn OFF the `enabled` field of directory. const handleSubmitInstanceUrl = () => { @@ -49,17 +43,16 @@ export default function PublicFacingDetails() { } const extraProps = { - // handleResetValue, initialValues, configPath: 'instanceDetails', }; return ( -
+ <> Edit your public facing instance details -
-
+
+
- - this page! - -
-
- {/* add social handles comp -
- add tags comp */} - - + + Edit your extra page content here. +
-
+ ); } diff --git a/web/pages/config-social-items.tsx b/web/pages/config-social-items.tsx new file mode 100644 index 000000000..6161ba5f8 --- /dev/null +++ b/web/pages/config-social-items.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Typography } from 'antd'; +import EditSocialLinks from './components/config/edit-social-links'; +import EditInstanceTags from './components/config/edit-tags'; +import EditDirectoryDetails from './components/config/edit-directory'; + +const { Title } = Typography; + +export default function ConfigSocialThings() { + return ( +
+ Social Items + + + + +
+ ); +} + diff --git a/web/styles/ant-overrides.scss b/web/styles/ant-overrides.scss new file mode 100644 index 000000000..0b41e7def --- /dev/null +++ b/web/styles/ant-overrides.scss @@ -0,0 +1,95 @@ +// GENERAL ANT OVERRIDES +.ant-layout, +.ant-layout-footer, +.ant-menu, +.ant-menu.ant-menu-dark { + background-color: transparent; +} +.owncast-layout .ant-menu-dark.ant-menu-dark:not(.ant-menu-horizontal) .ant-menu-item-selected { + background-color: var(--owncast-purple); +} + +// LAYOUT +.ant-layout-header, +.ant-layout-sider { + background-color: #07050d; +} + +// MENU +.ant-menu-dark .ant-menu-inline.ant-menu-sub { + // background-color: rgba(255,255,255,.05); + background-color: #140028; +} + +// CARD +.ant-card { + border-radius: .5em; +} + +// INPUT +.ant-input-affix-wrapper { + border-radius: 5px; + background-color: rgba(255,255,255,.1); + + textarea { + border-radius: 5px; + } + input { + background-color: transparent; + } +} + +// BUTTON +.ant-btn-primary:hover, .ant-btn-primary:focus { + background-color: white; + color: #40a9ff; +} +.ant-btn.ant-btn-primary:focus { + border-color: white; + +} +.ant-input-affix-wrapper, +.ant-btn { + transition-delay: 0s; + transition-duration: 0.15s; +} + +// TABLE +.ant-table-thead > tr > th, +.ant-table-small .ant-table-thead > tr > th { + background-color: #000; +} + +// MODAL +.ant-modal-content { + border-radius: 6px; +} +.ant-modal-header { + background-color: #1c173d; + border-radius: 6px 6px 0 0; +} +.ant-modal-title { + font-weight: bold; + font-size: 1.5em; +} +.ant-modal-body { + background-color: #33333c; +} +.ant-modal-footer { + background-color: #222229; +} + +// SELECT +.ant-select-dropdown { + background-color: #334; +} + + +// SLIDER +.ant-slider-with-marks { + margin-right: 2em; +} +.ant-slider-mark-text { + font-size: .85em; + white-space: nowrap; +} \ No newline at end of file diff --git a/web/styles/config-pages.module.scss b/web/styles/config-pages.module.scss new file mode 100644 index 000000000..4024444f5 --- /dev/null +++ b/web/styles/config-pages.module.scss @@ -0,0 +1,33 @@ +.publicDetailsContainer { + display: flex; + flex-direction: row; + align-items: flex-start; + flex-wrap: wrap; + + .textFieldsSection { + margin-right: 2rem; + } +} + + + +.socialLinksEditor { + width: 20rem; + margin: 2em 0; +} + + + +.tag-editor-container, +.config-directory-details-form { + border-radius: 1em; + background-color: rgba(128,99,255,.1); + padding: 1.5em; + margin-bottom: 1em; +} + +////////////////////////////// +// common? +.dataTable { + +} \ No newline at end of file diff --git a/web/styles/config.scss b/web/styles/config.scss index 7f3e1c8d9..d59d00ce3 100644 --- a/web/styles/config.scss +++ b/web/styles/config.scss @@ -26,13 +26,6 @@ margin-bottom: 1em; } -.ant-slider-with-marks { - margin-right: 2em; -} -.ant-slider-mark-text { - font-size: .85em; - white-space: nowrap; -} .status-message { margin: 1rem 0; diff --git a/web/styles/globals.scss b/web/styles/globals.scss index bc7931a20..eeef239d5 100644 --- a/web/styles/globals.scss +++ b/web/styles/globals.scss @@ -1,7 +1,5 @@ @import "~antd/dist/antd.dark"; -$owncast-purple: rgba(90,103,216,1); - html, body { padding: 0; @@ -37,84 +35,12 @@ code { color: var(--owncast-purple); } -.owncast-layout .ant-menu-dark.ant-menu-dark:not(.ant-menu-horizontal) .ant-menu-item-selected { - background-color: $owncast-purple; -} -// GENERAL ANT FORM OVERRIDES -.ant-layout, -.ant-layout-footer, -.ant-menu, -.ant-menu.ant-menu-dark { - background-color: transparent; -} -.ant-layout-header, -.ant-layout-sider { - background-color: #07050d; -} -.ant-menu-dark .ant-menu-inline.ant-menu-sub { - // background-color: rgba(255,255,255,.05); - background-color: #140028; -} -.ant-card { - border-radius: .5em; -} -.ant-input-affix-wrapper { - border-radius: 5px; - background-color: rgba(255,255,255,.1); - - textarea { - border-radius: 5px; - } - input { - background-color: transparent; - } -} -.ant-btn-primary:hover, .ant-btn-primary:focus { - background-color: white; - color: #40a9ff; -} -.ant-btn.ant-btn-primary:focus { - border-color: white; - -} -.ant-input-affix-wrapper, -.ant-btn { - transition-delay: 0s; - transition-duration: 0.15s; -} -.ant-table-thead > tr > th, -.ant-table-small .ant-table-thead > tr > th { - background-color: #000; -} -.ant-modal-content { - border-radius: 6px; -} -.ant-modal-header { - background-color: #1c173d; - border-radius: 6px 6px 0 0; -} -.ant-modal-title { - font-weight: bold; - font-size: 1.5em; -} -.ant-modal-body { - background-color: #33333c; -} -.ant-modal-footer { - background-color: #222229; -} - - -.ant-select-dropdown { - background-color: #334; -} -.rc-virtual-list-scrollbar { - display: block !important; -} - // markdown editor overrides +.rc-virtual-list-scrollbar { + display: block !important; +} .rc-md-editor { // Set the background color of the preview container .editor-container { @@ -126,7 +52,7 @@ code { .markdown-editor-preview-pane { // color:lightgrey; a { - color: $owncast-purple; + color: var(--owncast-purple);; } h1 { font-size: 2em; @@ -150,4 +76,4 @@ code { .button-type-undo, .button-type-redo, .button-type-clear, .button-type-image, .button-type-wrap, .button-type-quote, .button-type-strikethrough, .button-type-code-inline, .button-type-code-block { display: none !important; } -} \ No newline at end of file +} diff --git a/web/utils/apis.ts b/web/utils/apis.ts index 39acf45a7..d25679bb1 100644 --- a/web/utils/apis.ts +++ b/web/utils/apis.ts @@ -74,7 +74,7 @@ interface FetchOptions { auth?: boolean; }; -export async function fetchData(url: string, options?: object) { +export async function fetchData(url: string, options?: FetchOptions) { const { data, method = 'GET', @@ -129,23 +129,6 @@ export async function getGithubRelease() { return {}; } -// Make a request to the server status API and the Github releases API -// and return a release if it's newer than the server version. -export async function upgradeVersionAvailable(currentVersion) { - const recentRelease = await getGithubRelease(); - let recentReleaseVersion = recentRelease.tag_name; - - if (recentReleaseVersion.substr(0, 1) === 'v') { - recentReleaseVersion = recentReleaseVersion.substr(1) - } - - if (!upToDate(currentVersion, recentReleaseVersion)) { - return recentReleaseVersion - } - - return null; -} - // Stolen from https://gist.github.com/prenagha/98bbb03e27163bc2f5e4 const VPAT = /^\d+(\.\d+){0,2}$/; function upToDate(local, remote) { @@ -171,6 +154,22 @@ function upToDate(local, remote) { } return true; } - return local >= remote; - + return local >= remote; } + +// Make a request to the server status API and the Github releases API +// and return a release if it's newer than the server version. +export async function upgradeVersionAvailable(currentVersion) { + const recentRelease = await getGithubRelease(); + let recentReleaseVersion = recentRelease.tag_name; + + if (recentReleaseVersion.substr(0, 1) === 'v') { + recentReleaseVersion = recentReleaseVersion.substr(1) + } + + if (!upToDate(currentVersion, recentReleaseVersion)) { + return recentReleaseVersion + } + + return null; +} \ No newline at end of file