diff --git a/web/components/config/edit-custom-css.tsx b/web/components/config/edit-custom-css.tsx new file mode 100644 index 000000000..700530fb9 --- /dev/null +++ b/web/components/config/edit-custom-css.tsx @@ -0,0 +1,121 @@ +// EDIT CUSTOM CSS STYLES +import React, { useState, useEffect, useContext } from 'react'; +import { Typography, Button } from 'antd'; + +import { ServerStatusContext } from '../../utils/server-status-context'; +import { + postConfigUpdateToAPI, + RESET_TIMEOUT, + API_CUSTOM_CSS_STYLES, +} from '../../utils/config-constants'; +import { + createInputStatus, + StatusState, + STATUS_ERROR, + STATUS_PROCESSING, + STATUS_SUCCESS, +} from '../../utils/input-statuses'; +import FormStatusIndicator from './form-status-indicator'; + +import TextField, { TEXTFIELD_TYPE_TEXTAREA } from './form-textfield'; +import { UpdateArgs } from '../../types/config-section'; + +const { Title } = Typography; + +export default function EditCustomStyles() { + const [content, setContent] = useState(''); + const [submitStatus, setSubmitStatus] = useState(null); + const [hasChanged, setHasChanged] = useState(false); + + const serverStatusData = useContext(ServerStatusContext); + const { serverConfig, setFieldInConfigState } = serverStatusData || {}; + + const { instanceDetails } = serverConfig; + const { customStyles: initialContent } = instanceDetails; + + let resetTimer = null; + + function handleFieldChange({ value }: UpdateArgs) { + setContent(value); + if (value !== initialContent && !hasChanged) { + setHasChanged(true); + } else if (value === initialContent && hasChanged) { + setHasChanged(false); + } + } + + // Clear out any validation states and messaging + const resetStates = () => { + setSubmitStatus(null); + setHasChanged(false); + clearTimeout(resetTimer); + resetTimer = null; + }; + + // posts all the tags at once as an array obj + async function handleSave() { + setSubmitStatus(createInputStatus(STATUS_PROCESSING)); + await postConfigUpdateToAPI({ + apiPath: API_CUSTOM_CSS_STYLES, + data: { value: content }, + onSuccess: (message: string) => { + setFieldInConfigState({ + fieldName: 'customStyles', + value: content, + path: 'instanceDetails', + }); + setSubmitStatus(createInputStatus(STATUS_SUCCESS, message)); + }, + onError: (message: string) => { + setSubmitStatus(createInputStatus(STATUS_ERROR, message)); + }, + }); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + } + + useEffect(() => { + setContent(initialContent); + }, [instanceDetails]); + + return ( +
+ + Customize your page styling with CSS + + +

+ Customize the look and feel of your Owncast instance by overriding the CSS styles of various + components on the page. Refer to the{' '} + + CSS & Components guide + {' '} + in the code base for suggestions. +

+

+ Please input plain CSS text, as this will be directly injected onto your page during load. +

+ + +
+
+ {hasChanged && ( + + )} + +
+
+ ); +} diff --git a/web/pages/config-public-details.tsx b/web/pages/config-public-details.tsx index 0dba0ae97..438aee8e8 100644 --- a/web/pages/config-public-details.tsx +++ b/web/pages/config-public-details.tsx @@ -5,6 +5,7 @@ import EditInstanceDetails from '../components/config/edit-instance-details'; import EditInstanceTags from '../components/config/edit-tags'; import EditSocialLinks from '../components/config/edit-social-links'; import EditPageContent from '../components/config/edit-page-content'; +import EditCustomStyles from '../components/config/edit-custom-css'; const { Title } = Typography; @@ -41,6 +42,9 @@ export default function PublicFacingDetails() {
+
+ +
); } diff --git a/web/styles/config-public-details.scss b/web/styles/config-public-details.scss index 69835a009..f99997ba7 100644 --- a/web/styles/config-public-details.scss +++ b/web/styles/config-public-details.scss @@ -62,13 +62,21 @@ height: 6em !important; } } + + .edit-custom-css { + #field-customStyles { + height: 15em; + width: 100%; + font-family: monospace; + resize: auto; + } + } } .other-field-container { margin: 0.5em 0; } - .logo-upload-container { .input-group { align-items: center; diff --git a/web/types/config-section.ts b/web/types/config-section.ts index f41f409d2..c3e125c8e 100644 --- a/web/types/config-section.ts +++ b/web/types/config-section.ts @@ -28,6 +28,7 @@ export interface ConfigDirectoryFields { } export interface ConfigInstanceDetailsFields { + customStyles: string; extraPageContent: string; logo: string; name: string; @@ -35,9 +36,9 @@ export interface ConfigInstanceDetailsFields { socialHandles: SocialHandle[]; streamTitle: string; summary: string; - welcomeMessage: string; tags: string[]; title: string; + welcomeMessage: string; } export type CpuUsageLevel = 1 | 2 | 3 | 4 | 5; @@ -88,14 +89,14 @@ export interface ExternalAction { } export interface ConfigDetails { + chatDisabled: boolean; + externalActions: ExternalAction[]; ffmpegPath: string; instanceDetails: ConfigInstanceDetailsFields; rtmpServerPort: string; s3: S3Field; streamKey: string; + videoSettings: VideoSettingsFields; webServerPort: string; yp: ConfigDirectoryFields; - videoSettings: VideoSettingsFields; - chatDisabled: boolean; - externalActions: ExternalAction[]; } diff --git a/web/utils/config-constants.tsx b/web/utils/config-constants.tsx index 1412ed204..72f921920 100644 --- a/web/utils/config-constants.tsx +++ b/web/utils/config-constants.tsx @@ -8,6 +8,7 @@ export const RESET_TIMEOUT = 3000; // CONFIG API ENDPOINTS export const API_CUSTOM_CONTENT = '/pagecontent'; +export const API_CUSTOM_CSS_STYLES = '/customstyles'; export const API_FFMPEG = '/ffmpegpath'; export const API_INSTANCE_URL = '/serverurl'; export const API_LOGO = '/logo'; @@ -27,7 +28,7 @@ export const API_VIDEO_VARIANTS = '/video/streamoutputvariants'; export const API_WEB_PORT = '/webserverport'; export const API_YP_SWITCH = '/directoryenabled'; export const API_CHAT_DISABLE = '/chat/disable'; -export const API_EXTERNAL_ACTIONS = '/externalactions' +export const API_EXTERNAL_ACTIONS = '/externalactions'; export async function postConfigUpdateToAPI(args: ApiPostArgs) { const { apiPath, data, onSuccess, onError } = args; @@ -141,13 +142,6 @@ export const FIELD_PROPS_TAGS = { tip: '', }; -export const FIELD_PROPS_CUSTOM_CONTENT = { - apiPath: API_CUSTOM_CONTENT, - configPath: 'instanceDetails', - placeholder: '', - label: 'Extra page content', - tip: 'Custom markup about yourself', -}; export const FIELD_PROPS_NSFW = { apiPath: API_NSFW_SWITCH, configPath: 'instanceDetails', diff --git a/web/utils/server-status-context.tsx b/web/utils/server-status-context.tsx index 345b08842..ecc365e82 100644 --- a/web/utils/server-status-context.tsx +++ b/web/utils/server-status-context.tsx @@ -4,12 +4,13 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { STATUS, fetchData, FETCH_INTERVAL, SERVER_CONFIG } from './apis'; -import { ConfigDetails, UpdateArgs, ExternalAction } from '../types/config-section'; +import { ConfigDetails, UpdateArgs } from '../types/config-section'; import { DEFAULT_VARIANT_STATE } from './config-constants'; export const initialServerConfigState: ConfigDetails = { streamKey: '', instanceDetails: { + customStyles: '', extraPageContent: '', logo: '', name: '', @@ -17,9 +18,9 @@ export const initialServerConfigState: ConfigDetails = { socialHandles: [], streamTitle: '', summary: '', - welcomeMessage: '', tags: [], title: '', + welcomeMessage: '', }, ffmpegPath: '', rtmpServerPort: '',