Allow adding custom javascript to the page. Closes #2604
This commit is contained in:
118
web/components/admin/EditCustomJavascript.tsx
Normal file
118
web/components/admin/EditCustomJavascript.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { useState, useEffect, useContext, FC } from 'react';
|
||||
import { Typography, Button } from 'antd';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { bbedit } from '@uiw/codemirror-theme-bbedit';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import {
|
||||
postConfigUpdateToAPI,
|
||||
RESET_TIMEOUT,
|
||||
API_CUSTOM_JAVASCRIPT,
|
||||
} from '../../utils/config-constants';
|
||||
import {
|
||||
createInputStatus,
|
||||
StatusState,
|
||||
STATUS_ERROR,
|
||||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} from '../../utils/input-statuses';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const EditCustomJavascript: FC = () => {
|
||||
const [content, setContent] = useState('/* Enter custom Javascript here */');
|
||||
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
|
||||
const [hasChanged, setHasChanged] = useState(false);
|
||||
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||
|
||||
const { instanceDetails } = serverConfig;
|
||||
const { customJavascript: initialContent } = instanceDetails;
|
||||
|
||||
let resetTimer = null;
|
||||
|
||||
// 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_JAVASCRIPT,
|
||||
data: { value: content },
|
||||
onSuccess: (message: string) => {
|
||||
setFieldInConfigState({
|
||||
fieldName: 'customJavascript',
|
||||
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]);
|
||||
|
||||
const onCSSValueChange = React.useCallback(value => {
|
||||
setContent(value);
|
||||
if (value !== initialContent && !hasChanged) {
|
||||
setHasChanged(true);
|
||||
} else if (value === initialContent && hasChanged) {
|
||||
setHasChanged(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="edit-custom-css">
|
||||
<Title level={3} className="section-title">
|
||||
Customize your page styling with CSS
|
||||
</Title>
|
||||
|
||||
<p className="description">
|
||||
Customize the look and feel of your Owncast instance by overriding the CSS styles of various
|
||||
components on the page. Refer to the{' '}
|
||||
<a href="https://owncast.online/docs/website/" rel="noopener noreferrer" target="_blank">
|
||||
CSS & Components guide
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className="description">
|
||||
Please input plain CSS text, as this will be directly injected onto your page during load.
|
||||
</p>
|
||||
|
||||
<CodeMirror
|
||||
value={content}
|
||||
placeholder="/* Enter custom Javascript here */"
|
||||
theme={bbedit}
|
||||
height="200px"
|
||||
extensions={[javascript()]}
|
||||
onChange={onCSSValueChange}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<div className="page-content-actions">
|
||||
{hasChanged && (
|
||||
<Button type="primary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
<FormStatusIndicator status={submitStatus} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import Head from 'next/head';
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import { Layout } from 'antd';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Script from 'next/script';
|
||||
import {
|
||||
ClientConfigStore,
|
||||
isChatAvailableSelector,
|
||||
@@ -133,6 +134,8 @@ export const Main: FC = () => {
|
||||
<PushNotificationServiceWorker />
|
||||
<TitleNotifier name={name} />
|
||||
<Theme />
|
||||
<Script strategy="afterInteractive" src="/customjavascript" />
|
||||
|
||||
<Layout ref={layoutRef} className={styles.layout}>
|
||||
<Header name={title || name} chatAvailable={isChatAvailable} chatDisabled={chatDisabled} />
|
||||
<Content />
|
||||
|
||||
@@ -43,6 +43,10 @@ module.exports = withBundleAnalyzer(
|
||||
source: '/thumbnail.jpg',
|
||||
destination: 'http://localhost:8080/thumbnail.jpg', // Proxy to Backend to work around CORS.
|
||||
},
|
||||
{
|
||||
source: '/customjavascript',
|
||||
destination: 'http://localhost:8080/customjavascript', // Proxy to Backend to work around CORS.
|
||||
},
|
||||
];
|
||||
},
|
||||
pageExtensions: ['tsx'],
|
||||
|
||||
1
web/package-lock.json
generated
1
web/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "4.8.0",
|
||||
"@codemirror/lang-css": "6.0.1",
|
||||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/lang-markdown": "6.0.5",
|
||||
"@codemirror/language-data": "6.1.0",
|
||||
"@fontsource/open-sans": "4.5.13",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "4.8.0",
|
||||
"@codemirror/lang-css": "6.0.1",
|
||||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/lang-markdown": "6.0.5",
|
||||
"@codemirror/language-data": "6.1.0",
|
||||
"@fontsource/open-sans": "4.5.13",
|
||||
@@ -53,11 +54,11 @@
|
||||
"ua-parser-js": "1.0.32",
|
||||
"video.js": "7.20.3",
|
||||
"xstate": "4.35.2",
|
||||
"yaml": "2.2.1"
|
||||
"yaml": "2.2.1",
|
||||
"@next/bundle-analyzer": "^13.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.20.12",
|
||||
"style-dictionary": "3.7.2",
|
||||
"@mdx-js/react": "2.2.1",
|
||||
"@storybook/addon-a11y": "6.5.15",
|
||||
"@storybook/addon-actions": "6.5.15",
|
||||
@@ -73,8 +74,6 @@
|
||||
"@storybook/preset-scss": "1.0.3",
|
||||
"@storybook/react": "6.5.15",
|
||||
"@storybook/testing-library": "0.0.13",
|
||||
"storybook-addon-designs": "6.3.1",
|
||||
"storybook-addon-fetch-mock": "1.0.1",
|
||||
"@svgr/webpack": "6.5.1",
|
||||
"@types/chart.js": "2.9.37",
|
||||
"@types/classnames": "2.3.1",
|
||||
@@ -108,10 +107,12 @@
|
||||
"sass": "1.57.1",
|
||||
"sass-loader": "13.2.0",
|
||||
"sb": "6.5.15",
|
||||
"storybook-addon-designs": "6.3.1",
|
||||
"storybook-addon-fetch-mock": "1.0.1",
|
||||
"storybook-dark-mode": "2.0.5",
|
||||
"storybook-preset-less": "1.1.3",
|
||||
"style-dictionary": "3.7.2",
|
||||
"style-loader": "3.3.1",
|
||||
"typescript": "4.9.4",
|
||||
"@next/bundle-analyzer": "^13.1.1"
|
||||
"typescript": "4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import GeneralConfig from '../../../../components/admin/config/general/GeneralCo
|
||||
import AppearanceConfig from '../../../../components/admin/config/general/AppearanceConfig';
|
||||
|
||||
import { AdminLayout } from '../../../../components/layouts/AdminLayout';
|
||||
import { EditCustomJavascript } from '../../../../components/admin/EditCustomJavascript';
|
||||
|
||||
export default function PublicFacingDetails() {
|
||||
return (
|
||||
@@ -23,6 +24,11 @@ export default function PublicFacingDetails() {
|
||||
key: '2',
|
||||
children: <AppearanceConfig />,
|
||||
},
|
||||
{
|
||||
label: `Custom Scripting`,
|
||||
key: '3',
|
||||
children: <EditCustomJavascript />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface ConfigDirectoryFields {
|
||||
|
||||
export interface ConfigInstanceDetailsFields {
|
||||
customStyles: string;
|
||||
customJavascript: string;
|
||||
extraPageContent: string;
|
||||
logo: string;
|
||||
name: string;
|
||||
|
||||
@@ -11,6 +11,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_CUSTOM_JAVASCRIPT = '/customjavascript';
|
||||
export const API_FFMPEG = '/ffmpegpath';
|
||||
export const API_INSTANCE_URL = '/serverurl';
|
||||
export const API_LOGO = '/logo';
|
||||
|
||||
@@ -12,6 +12,7 @@ export const initialServerConfigState: ConfigDetails = {
|
||||
adminPassword: '',
|
||||
instanceDetails: {
|
||||
customStyles: '',
|
||||
customJavascript: '',
|
||||
extraPageContent: '',
|
||||
logo: '',
|
||||
name: '',
|
||||
|
||||
Reference in New Issue
Block a user