mirror of
				https://github.com/owncast/owncast.git
				synced 2025-10-31 18:18:06 +08:00 
			
		
		
		
	Allow adding custom javascript to the page. Closes #2604
This commit is contained in:
		| @ -650,6 +650,22 @@ func SetCustomStyles(w http.ResponseWriter, r *http.Request) { | |||||||
| 	controllers.WriteSimpleResponse(w, true, "custom styles updated") | 	controllers.WriteSimpleResponse(w, true, "custom styles updated") | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // SetCustomJavascript will set the Javascript string we insert into the page. | ||||||
|  | func SetCustomJavascript(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	customJavascript, success := getValueFromRequest(w, r) | ||||||
|  | 	if !success { | ||||||
|  | 		controllers.WriteSimpleResponse(w, false, "unable to update custom javascript") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := data.SetCustomJavascript(customJavascript.Value.(string)); err != nil { | ||||||
|  | 		controllers.WriteSimpleResponse(w, false, err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	controllers.WriteSimpleResponse(w, true, "custom styles updated") | ||||||
|  | } | ||||||
|  |  | ||||||
| // SetForbiddenUsernameList will set the list of usernames we do not allow to use. | // SetForbiddenUsernameList will set the list of usernames we do not allow to use. | ||||||
| func SetForbiddenUsernameList(w http.ResponseWriter, r *http.Request) { | func SetForbiddenUsernameList(w http.ResponseWriter, r *http.Request) { | ||||||
| 	type forbiddenUsernameListRequest struct { | 	type forbiddenUsernameListRequest struct { | ||||||
|  | |||||||
| @ -46,6 +46,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { | |||||||
| 			SocialHandles:       data.GetSocialHandles(), | 			SocialHandles:       data.GetSocialHandles(), | ||||||
| 			NSFW:                data.GetNSFW(), | 			NSFW:                data.GetNSFW(), | ||||||
| 			CustomStyles:        data.GetCustomStyles(), | 			CustomStyles:        data.GetCustomStyles(), | ||||||
|  | 			CustomJavascript:    data.GetCustomJavascript(), | ||||||
| 			AppearanceVariables: data.GetCustomColorVariableValues(), | 			AppearanceVariables: data.GetCustomColorVariableValues(), | ||||||
| 		}, | 		}, | ||||||
| 		FFmpegPath:              ffmpeg, | 		FFmpegPath:              ffmpeg, | ||||||
| @ -138,6 +139,7 @@ type webConfigResponse struct { | |||||||
| 	StreamTitle         string                `json:"streamTitle"` // What's going on with the current stream | 	StreamTitle         string                `json:"streamTitle"` // What's going on with the current stream | ||||||
| 	SocialHandles       []models.SocialHandle `json:"socialHandles"` | 	SocialHandles       []models.SocialHandle `json:"socialHandles"` | ||||||
| 	CustomStyles        string                `json:"customStyles"` | 	CustomStyles        string                `json:"customStyles"` | ||||||
|  | 	CustomJavascript    string                `json:"customJavascript"` | ||||||
| 	AppearanceVariables map[string]string     `json:"appearanceVariables"` | 	AppearanceVariables map[string]string     `json:"appearanceVariables"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								controllers/customJavascript.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								controllers/customJavascript.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | package controllers | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/owncast/owncast/core/data" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ServeCustomJavascript will serve optional custom Javascript. | ||||||
|  | func ServeCustomJavascript(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	js := data.GetCustomJavascript() | ||||||
|  | 	w.Write([]byte(js)) | ||||||
|  | } | ||||||
| @ -44,6 +44,7 @@ const ( | |||||||
| 	chatDisabledKey                      = "chat_disabled" | 	chatDisabledKey                      = "chat_disabled" | ||||||
| 	externalActionsKey                   = "external_actions" | 	externalActionsKey                   = "external_actions" | ||||||
| 	customStylesKey                      = "custom_styles" | 	customStylesKey                      = "custom_styles" | ||||||
|  | 	customJavascriptKey                  = "custom_javascript" | ||||||
| 	videoCodecKey                        = "video_codec" | 	videoCodecKey                        = "video_codec" | ||||||
| 	blockedUsernamesKey                  = "blocked_usernames" | 	blockedUsernamesKey                  = "blocked_usernames" | ||||||
| 	publicKeyKey                         = "public_key" | 	publicKeyKey                         = "public_key" | ||||||
| @ -560,6 +561,21 @@ func GetCustomStyles() string { | |||||||
| 	return style | 	return style | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // SetCustomJavascript will save a string with Javascript to insert into the page. | ||||||
|  | func SetCustomJavascript(styles string) error { | ||||||
|  | 	return _datastore.SetString(customJavascriptKey, styles) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetCustomJavascript will return a string with Javascript to insert into the page. | ||||||
|  | func GetCustomJavascript() string { | ||||||
|  | 	style, err := _datastore.GetString(customJavascriptKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return style | ||||||
|  | } | ||||||
|  |  | ||||||
| // SetVideoCodec will set the codec used for video encoding. | // SetVideoCodec will set the codec used for video encoding. | ||||||
| func SetVideoCodec(codec string) error { | func SetVideoCodec(codec string) error { | ||||||
| 	return _datastore.SetString(videoCodecKey, codec) | 	return _datastore.SetString(videoCodecKey, codec) | ||||||
|  | |||||||
| @ -39,6 +39,9 @@ func Start() error { | |||||||
| 	http.HandleFunc("/preview.gif", controllers.GetPreview) | 	http.HandleFunc("/preview.gif", controllers.GetPreview) | ||||||
| 	http.HandleFunc("/logo", controllers.GetLogo) | 	http.HandleFunc("/logo", controllers.GetLogo) | ||||||
|  |  | ||||||
|  | 	// Custom Javascript | ||||||
|  | 	http.HandleFunc("/customjavascript", controllers.ServeCustomJavascript) | ||||||
|  |  | ||||||
| 	// Return a single emoji image. | 	// Return a single emoji image. | ||||||
| 	http.HandleFunc(config.EmojiDir, controllers.GetCustomEmojiImage) | 	http.HandleFunc(config.EmojiDir, controllers.GetCustomEmojiImage) | ||||||
|  |  | ||||||
| @ -315,6 +318,9 @@ func Start() error { | |||||||
| 	// set custom style css | 	// set custom style css | ||||||
| 	http.HandleFunc("/api/admin/config/customstyles", middleware.RequireAdminAuth(admin.SetCustomStyles)) | 	http.HandleFunc("/api/admin/config/customstyles", middleware.RequireAdminAuth(admin.SetCustomStyles)) | ||||||
|  |  | ||||||
|  | 	// set custom style javascript | ||||||
|  | 	http.HandleFunc("/api/admin/config/customjavascript", middleware.RequireAdminAuth(admin.SetCustomJavascript)) | ||||||
|  |  | ||||||
| 	// Video playback metrics | 	// Video playback metrics | ||||||
| 	http.HandleFunc("/api/admin/metrics/video", middleware.RequireAdminAuth(admin.GetVideoPlaybackMetrics)) | 	http.HandleFunc("/api/admin/metrics/video", middleware.RequireAdminAuth(admin.GetVideoPlaybackMetrics)) | ||||||
|  |  | ||||||
|  | |||||||
| @ -132,6 +132,8 @@ const newFederationConfig = { | |||||||
| const newHideViewerCount = !defaultHideViewerCount; | const newHideViewerCount = !defaultHideViewerCount; | ||||||
|  |  | ||||||
| const overriddenWebsocketHost = 'ws://lolcalhost.biz'; | const overriddenWebsocketHost = 'ws://lolcalhost.biz'; | ||||||
|  | const customCSS = randomString(); | ||||||
|  | const customJavascript = randomString(); | ||||||
|  |  | ||||||
| test('verify default config values', async (done) => { | test('verify default config values', async (done) => { | ||||||
| 	const res = await request.get('/api/config'); | 	const res = await request.get('/api/config'); | ||||||
| @ -315,6 +317,16 @@ test('set custom style values', async (done) => { | |||||||
| 	done(); | 	done(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | test('set custom css', async (done) => { | ||||||
|  | 	await sendAdminRequest('config/customstyles', customCSS); | ||||||
|  | 	done(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | test('set custom javascript', async (done) => { | ||||||
|  | 	await sendAdminRequest('config/customjavascript', customJavascript); | ||||||
|  | 	done(); | ||||||
|  | }); | ||||||
|  |  | ||||||
| test('enable directory', async (done) => { | test('enable directory', async (done) => { | ||||||
| 	const res = await sendAdminRequest('config/directoryenabled', true); | 	const res = await sendAdminRequest('config/directoryenabled', true); | ||||||
| 	done(); | 	done(); | ||||||
| @ -367,6 +379,7 @@ test('verify updated config values', async (done) => { | |||||||
| 	expect(res.body.logo).toBe('/logo'); | 	expect(res.body.logo).toBe('/logo'); | ||||||
| 	expect(res.body.socialHandles).toStrictEqual(newSocialHandles); | 	expect(res.body.socialHandles).toStrictEqual(newSocialHandles); | ||||||
| 	expect(res.body.socketHostOverride).toBe(overriddenWebsocketHost); | 	expect(res.body.socketHostOverride).toBe(overriddenWebsocketHost); | ||||||
|  | 	expect(res.body.customStyles).toBe(customCSS); | ||||||
| 	done(); | 	done(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| @ -393,6 +406,9 @@ test('verify updated admin configuration', async (done) => { | |||||||
| 	expect(res.body.instanceDetails.socialHandles).toStrictEqual( | 	expect(res.body.instanceDetails.socialHandles).toStrictEqual( | ||||||
| 		newSocialHandles | 		newSocialHandles | ||||||
| 	); | 	); | ||||||
|  | 	expect(res.body.instanceDetails.customStyles).toBe(customCSS); | ||||||
|  | 	expect(res.body.instanceDetails.customJavascript).toBe(customJavascript); | ||||||
|  |  | ||||||
| 	expect(res.body.forbiddenUsernames).toStrictEqual(newForbiddenUsernames); | 	expect(res.body.forbiddenUsernames).toStrictEqual(newForbiddenUsernames); | ||||||
| 	expect(res.body.streamKeys).toStrictEqual(newStreamKeys); | 	expect(res.body.streamKeys).toStrictEqual(newStreamKeys); | ||||||
| 	expect(res.body.socketHostOverride).toBe(overriddenWebsocketHost); | 	expect(res.body.socketHostOverride).toBe(overriddenWebsocketHost); | ||||||
|  | |||||||
							
								
								
									
										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 { FC, useEffect, useRef } from 'react'; | ||||||
| import { Layout } from 'antd'; | import { Layout } from 'antd'; | ||||||
| import dynamic from 'next/dynamic'; | import dynamic from 'next/dynamic'; | ||||||
|  | import Script from 'next/script'; | ||||||
| import { | import { | ||||||
|   ClientConfigStore, |   ClientConfigStore, | ||||||
|   isChatAvailableSelector, |   isChatAvailableSelector, | ||||||
| @ -133,6 +134,8 @@ export const Main: FC = () => { | |||||||
|       <PushNotificationServiceWorker /> |       <PushNotificationServiceWorker /> | ||||||
|       <TitleNotifier name={name} /> |       <TitleNotifier name={name} /> | ||||||
|       <Theme /> |       <Theme /> | ||||||
|  |       <Script strategy="afterInteractive" src="/customjavascript" /> | ||||||
|  |  | ||||||
|       <Layout ref={layoutRef} className={styles.layout}> |       <Layout ref={layoutRef} className={styles.layout}> | ||||||
|         <Header name={title || name} chatAvailable={isChatAvailable} chatDisabled={chatDisabled} /> |         <Header name={title || name} chatAvailable={isChatAvailable} chatDisabled={chatDisabled} /> | ||||||
|         <Content /> |         <Content /> | ||||||
|  | |||||||
| @ -43,6 +43,10 @@ module.exports = withBundleAnalyzer( | |||||||
|           source: '/thumbnail.jpg', |           source: '/thumbnail.jpg', | ||||||
|           destination: 'http://localhost:8080/thumbnail.jpg', // Proxy to Backend to work around CORS. |           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'], |     pageExtensions: ['tsx'], | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -10,6 +10,7 @@ | |||||||
| 			"dependencies": { | 			"dependencies": { | ||||||
| 				"@ant-design/icons": "4.8.0", | 				"@ant-design/icons": "4.8.0", | ||||||
| 				"@codemirror/lang-css": "6.0.1", | 				"@codemirror/lang-css": "6.0.1", | ||||||
|  | 				"@codemirror/lang-javascript": "^6.1.2", | ||||||
| 				"@codemirror/lang-markdown": "6.0.5", | 				"@codemirror/lang-markdown": "6.0.5", | ||||||
| 				"@codemirror/language-data": "6.1.0", | 				"@codemirror/language-data": "6.1.0", | ||||||
| 				"@fontsource/open-sans": "4.5.13", | 				"@fontsource/open-sans": "4.5.13", | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ | |||||||
| 	"dependencies": { | 	"dependencies": { | ||||||
| 		"@ant-design/icons": "4.8.0", | 		"@ant-design/icons": "4.8.0", | ||||||
| 		"@codemirror/lang-css": "6.0.1", | 		"@codemirror/lang-css": "6.0.1", | ||||||
|  | 		"@codemirror/lang-javascript": "^6.1.2", | ||||||
| 		"@codemirror/lang-markdown": "6.0.5", | 		"@codemirror/lang-markdown": "6.0.5", | ||||||
| 		"@codemirror/language-data": "6.1.0", | 		"@codemirror/language-data": "6.1.0", | ||||||
| 		"@fontsource/open-sans": "4.5.13", | 		"@fontsource/open-sans": "4.5.13", | ||||||
| @ -53,11 +54,11 @@ | |||||||
| 		"ua-parser-js": "1.0.32", | 		"ua-parser-js": "1.0.32", | ||||||
| 		"video.js": "7.20.3", | 		"video.js": "7.20.3", | ||||||
| 		"xstate": "4.35.2", | 		"xstate": "4.35.2", | ||||||
| 		"yaml": "2.2.1" | 		"yaml": "2.2.1", | ||||||
|  | 		"@next/bundle-analyzer": "^13.1.1" | ||||||
| 	}, | 	}, | ||||||
| 	"devDependencies": { | 	"devDependencies": { | ||||||
| 		"@babel/core": "7.20.12", | 		"@babel/core": "7.20.12", | ||||||
| 		"style-dictionary": "3.7.2", |  | ||||||
| 		"@mdx-js/react": "2.2.1", | 		"@mdx-js/react": "2.2.1", | ||||||
| 		"@storybook/addon-a11y": "6.5.15", | 		"@storybook/addon-a11y": "6.5.15", | ||||||
| 		"@storybook/addon-actions": "6.5.15", | 		"@storybook/addon-actions": "6.5.15", | ||||||
| @ -73,8 +74,6 @@ | |||||||
| 		"@storybook/preset-scss": "1.0.3", | 		"@storybook/preset-scss": "1.0.3", | ||||||
| 		"@storybook/react": "6.5.15", | 		"@storybook/react": "6.5.15", | ||||||
| 		"@storybook/testing-library": "0.0.13", | 		"@storybook/testing-library": "0.0.13", | ||||||
| 		"storybook-addon-designs": "6.3.1", |  | ||||||
| 		"storybook-addon-fetch-mock": "1.0.1", |  | ||||||
| 		"@svgr/webpack": "6.5.1", | 		"@svgr/webpack": "6.5.1", | ||||||
| 		"@types/chart.js": "2.9.37", | 		"@types/chart.js": "2.9.37", | ||||||
| 		"@types/classnames": "2.3.1", | 		"@types/classnames": "2.3.1", | ||||||
| @ -108,10 +107,12 @@ | |||||||
| 		"sass": "1.57.1", | 		"sass": "1.57.1", | ||||||
| 		"sass-loader": "13.2.0", | 		"sass-loader": "13.2.0", | ||||||
| 		"sb": "6.5.15", | 		"sb": "6.5.15", | ||||||
|  | 		"storybook-addon-designs": "6.3.1", | ||||||
|  | 		"storybook-addon-fetch-mock": "1.0.1", | ||||||
| 		"storybook-dark-mode": "2.0.5", | 		"storybook-dark-mode": "2.0.5", | ||||||
| 		"storybook-preset-less": "1.1.3", | 		"storybook-preset-less": "1.1.3", | ||||||
|  | 		"style-dictionary": "3.7.2", | ||||||
| 		"style-loader": "3.3.1", | 		"style-loader": "3.3.1", | ||||||
| 		"typescript": "4.9.4", | 		"typescript": "4.9.4" | ||||||
| 		"@next/bundle-analyzer": "^13.1.1" |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import GeneralConfig from '../../../../components/admin/config/general/GeneralCo | |||||||
| import AppearanceConfig from '../../../../components/admin/config/general/AppearanceConfig'; | import AppearanceConfig from '../../../../components/admin/config/general/AppearanceConfig'; | ||||||
|  |  | ||||||
| import { AdminLayout } from '../../../../components/layouts/AdminLayout'; | import { AdminLayout } from '../../../../components/layouts/AdminLayout'; | ||||||
|  | import { EditCustomJavascript } from '../../../../components/admin/EditCustomJavascript'; | ||||||
|  |  | ||||||
| export default function PublicFacingDetails() { | export default function PublicFacingDetails() { | ||||||
|   return ( |   return ( | ||||||
| @ -23,6 +24,11 @@ export default function PublicFacingDetails() { | |||||||
|             key: '2', |             key: '2', | ||||||
|             children: <AppearanceConfig />, |             children: <AppearanceConfig />, | ||||||
|           }, |           }, | ||||||
|  |           { | ||||||
|  |             label: `Custom Scripting`, | ||||||
|  |             key: '3', | ||||||
|  |             children: <EditCustomJavascript />, | ||||||
|  |           }, | ||||||
|         ]} |         ]} | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -29,6 +29,7 @@ export interface ConfigDirectoryFields { | |||||||
|  |  | ||||||
| export interface ConfigInstanceDetailsFields { | export interface ConfigInstanceDetailsFields { | ||||||
|   customStyles: string; |   customStyles: string; | ||||||
|  |   customJavascript: string; | ||||||
|   extraPageContent: string; |   extraPageContent: string; | ||||||
|   logo: string; |   logo: string; | ||||||
|   name: string; |   name: string; | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ export const RESET_TIMEOUT = 3000; | |||||||
| // CONFIG API ENDPOINTS | // CONFIG API ENDPOINTS | ||||||
| export const API_CUSTOM_CONTENT = '/pagecontent'; | export const API_CUSTOM_CONTENT = '/pagecontent'; | ||||||
| export const API_CUSTOM_CSS_STYLES = '/customstyles'; | export const API_CUSTOM_CSS_STYLES = '/customstyles'; | ||||||
|  | export const API_CUSTOM_JAVASCRIPT = '/customjavascript'; | ||||||
| export const API_FFMPEG = '/ffmpegpath'; | export const API_FFMPEG = '/ffmpegpath'; | ||||||
| export const API_INSTANCE_URL = '/serverurl'; | export const API_INSTANCE_URL = '/serverurl'; | ||||||
| export const API_LOGO = '/logo'; | export const API_LOGO = '/logo'; | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ export const initialServerConfigState: ConfigDetails = { | |||||||
|   adminPassword: '', |   adminPassword: '', | ||||||
|   instanceDetails: { |   instanceDetails: { | ||||||
|     customStyles: '', |     customStyles: '', | ||||||
|  |     customJavascript: '', | ||||||
|     extraPageContent: '', |     extraPageContent: '', | ||||||
|     logo: '', |     logo: '', | ||||||
|     name: '', |     name: '', | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Gabe Kangas
					Gabe Kangas