mirror of
https://github.com/owncast/owncast.git
synced 2024-10-10 19:16:02 +00:00
reorganize styles and pages (wip); update readme
This commit is contained in:
parent
8ddd780281
commit
ff51c168ff
@ -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';
|
||||
|
@ -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 `<Form>` 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>` + `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 <Form> 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?
|
||||
- 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
|
@ -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 (
|
||||
<div className="config-social-links">
|
||||
<div className={configStyles.socialLinksEditor}>
|
||||
<Title level={2}>Social Links</Title>
|
||||
<p>Add all your social media handles and links to your other profiles here.</p>
|
||||
|
||||
{statusMessage}
|
||||
|
||||
<Table
|
||||
className="variants-table"
|
||||
className="dataTable"
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowKey={record => record.url}
|
@ -155,10 +155,9 @@ export default function TextField(props: TextFieldProps) {
|
||||
|
||||
return (
|
||||
<div className={`textfield-container type-${type}`}>
|
||||
<div className="textfield-label">{label}</div>
|
||||
<div className="textfield">
|
||||
<InfoTip tip={tip} />
|
||||
<Form.Item
|
||||
label={label}
|
||||
name={fieldName}
|
||||
hasFeedback
|
||||
validateStatus={submitStatus}
|
||||
@ -176,8 +175,8 @@ export default function TextField(props: TextFieldProps) {
|
||||
{...fieldProps}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
</div>
|
||||
<InfoTip tip={tip} />
|
||||
|
||||
{ hasChanged ? <Button type="primary" size="small" className="submit-button" onClick={handleSubmit}>Update</Button> : null }
|
||||
|
||||
|
@ -141,12 +141,14 @@ export default function MainLayout(props) {
|
||||
<Menu.Item key="config-public-details">
|
||||
<Link href="/config-public-details">Public Details</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="config-social-items">
|
||||
<Link href="/config-social-items">Social items</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item key="config-page-content">
|
||||
<Link href="/config-page-content">Custom page content</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="config-social-links">
|
||||
<Link href="/config-social-links">Social links</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item key="config-server-details">
|
||||
<Link href="/config-server-details">Server Details</Link>
|
||||
</Menu.Item>
|
||||
|
@ -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 (
|
||||
<div className="config-public-details-form">
|
||||
<>
|
||||
<Title level={2}>Edit your public facing instance details</Title>
|
||||
|
||||
<div className="config-public-details-container">
|
||||
<div className="text-fields">
|
||||
<div className={configStyles.publicDetailsContainer}>
|
||||
<div className={configStyles.textFieldsSection}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@ -78,20 +71,13 @@ export default function PublicFacingDetails() {
|
||||
<TextField fieldName="summary" type={TEXTFIELD_TYPE_TEXTAREA} {...extraProps} />
|
||||
<TextField fieldName="logo" {...extraProps} />
|
||||
</Form>
|
||||
<Link href="/admin/config-page-content">
|
||||
<a>this page!</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="misc-fields">
|
||||
{/* add social handles comp
|
||||
<br/>
|
||||
add tags comp */}
|
||||
<EditInstanceTags />
|
||||
|
||||
<EditDirectoryDetails />
|
||||
<Link href="/admin/config-page-content">
|
||||
<a>Edit your extra page content here.</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
20
web/pages/config-social-items.tsx
Normal file
20
web/pages/config-social-items.tsx
Normal file
@ -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 (
|
||||
<div className="config-social-items">
|
||||
<Title level={2}>Social Items</Title>
|
||||
|
||||
<EditDirectoryDetails />
|
||||
<EditSocialLinks />
|
||||
<EditInstanceTags />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
95
web/styles/ant-overrides.scss
Normal file
95
web/styles/ant-overrides.scss
Normal file
@ -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;
|
||||
}
|
33
web/styles/config-pages.module.scss
Normal file
33
web/styles/config-pages.module.scss
Normal file
@ -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 {
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user