reorganize styles and pages (wip); update readme

This commit is contained in:
gingervitis 2021-01-27 01:46:08 -08:00
parent 8ddd780281
commit ff51c168ff
12 changed files with 254 additions and 146 deletions

View File

@ -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';

View File

@ -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

View File

@ -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}

View File

@ -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 }

View File

@ -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>

View File

@ -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>
</>
);
}

View 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>
);
}

View 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;
}

View 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 {
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;
}