diff --git a/web/pages/components/config/constants.tsx b/web/pages/components/config/constants.tsx
index 9e9d047df..1fc7d357b 100644
--- a/web/pages/components/config/constants.tsx
+++ b/web/pages/components/config/constants.tsx
@@ -19,13 +19,14 @@ export const SUCCESS_STATES = {
},
error: {
icon: ,
- message: 'An error occurred.',
+ message: 'An error occurred.',
},
};
// CONFIG API ENDPOINTS
export const API_VIDEO_VARIANTS = '/video/streamoutputvariants';
export const API_VIDEO_SEGMENTS = '/video/streamlatencylevel';
+export const API_SOCIAL_HANDLES = '/socialhandles';
export async function postConfigUpdateToAPI(args: ApiPostArgs) {
const {
diff --git a/web/pages/components/config/edit-social-links.tsx b/web/pages/components/config/edit-social-links.tsx
new file mode 100644
index 000000000..8b483e0ec
--- /dev/null
+++ b/web/pages/components/config/edit-social-links.tsx
@@ -0,0 +1,132 @@
+import React, { useContext, useState, useEffect } from 'react';
+import { Typography, Input } from 'antd';
+
+import { ServerStatusContext } from '../../../utils/server-status-context';
+import { TEXTFIELD_DEFAULTS, RESET_TIMEOUT, SUCCESS_STATES, postConfigUpdateToAPI } from './constants';
+
+const { Title } = Typography;
+
+export default function EditSocialLinks() {
+ const [newTagInput, setNewTagInput] = useState('');
+ const [submitStatus, setSubmitStatus] = useState(null);
+ const [submitStatusMessage, setSubmitStatusMessage] = useState('');
+ const serverStatusData = useContext(ServerStatusContext);
+ const { serverConfig, setFieldInConfigState } = serverStatusData || {};
+
+ const { instanceDetails } = serverConfig;
+ const { tags = [] } = instanceDetails;
+
+ const configPath = 'instanceDetails';
+
+ const {
+ apiPath,
+ maxLength,
+ placeholder,
+ } = TEXTFIELD_DEFAULTS[configPath].tags || {};
+
+
+ let resetTimer = null;
+
+ useEffect(() => {
+ return () => {
+ clearTimeout(resetTimer);
+ }
+ }, []);
+
+ const resetStates = () => {
+ setSubmitStatus(null);
+ setSubmitStatusMessage('');
+ resetTimer = null;
+ clearTimeout(resetTimer);
+ }
+
+ // posts all the tags at once as an array obj
+ const postUpdateToAPI = async (postValue: any) => {
+ await postConfigUpdateToAPI({
+ apiPath,
+ data: { value: postValue },
+ onSuccess: () => {
+ setFieldInConfigState({ fieldName: 'socialHandles', value: postValue, path: configPath });
+ setSubmitStatus('success');
+ setSubmitStatusMessage('Tags updated.');
+ setNewTagInput('');
+ resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
+ },
+ onError: (message: string) => {
+ setSubmitStatus('error');
+ setSubmitStatusMessage(message);
+ resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
+ },
+ });
+ };
+
+ const handleInputChange = e => {
+ if (submitStatusMessage !== '') {
+ setSubmitStatusMessage('');
+ }
+ setNewTagInput(e.target.value);
+ };
+
+ // send to api and do stuff
+ const handleSubmitNewLink = () => {
+ resetStates();
+ const newTag = newTagInput.trim();
+ if (newTag === '') {
+ setSubmitStatusMessage('Please enter a tag');
+ return;
+ }
+ if (tags.some(tag => tag.toLowerCase() === newTag.toLowerCase())) {
+ setSubmitStatusMessage('This tag is already used!');
+ return;
+ }
+
+ const updatedTags = [...tags, newTag];
+ postUpdateToAPI(updatedTags);
+ };
+
+ const handleDeleteLink = index => {
+ resetStates();
+ const updatedTags = [...tags];
+ updatedTags.splice(index, 1);
+ postUpdateToAPI(updatedTags);
+ }
+
+ const {
+ icon: newStatusIcon = null,
+ message: newStatusMessage = '',
+ } = SUCCESS_STATES[submitStatus] || {};
+
+ return (
+
+
+
Add Tags
+
This is a great way to categorize your Owncast server on the Directory!
+
+
+ {tags.map((tag, index) => {
+ const handleClose = () => {
+ handleDeleteLink(index);
+ };
+ return (
+ {tag}
+ );
+ })}
+
+
+ {newStatusIcon} {newStatusMessage} {submitStatusMessage}
+
+
+
+
+
+ );
+}
diff --git a/web/pages/components/config/social-icons-dropdown.tsx b/web/pages/components/config/social-icons-dropdown.tsx
new file mode 100644
index 000000000..1a4e8732a
--- /dev/null
+++ b/web/pages/components/config/social-icons-dropdown.tsx
@@ -0,0 +1,81 @@
+import React, { useState } from 'react';
+import { PlusOutlined } from "@ant-design/icons";
+import { Select, Divider, Input } from "antd";
+import classNames from 'classnames';
+import { SocialHandleItem } from "../../../types/config-section";
+import { NEXT_PUBLIC_API_HOST } from '../../../utils/apis';
+
+
+interface DropdownProps {
+ iconList: SocialHandleItem[];
+ selectedOption?: string;
+}
+interface DropdownOptionProps extends SocialHandleItem {
+ isSelected: boolean;
+}
+
+// Add "Other" item which creates a text field
+// Add fixed custom ones - "url", "donate", "follow", "rss"
+
+function dropdownRender(menu) {
+ console.log({menu})
+ return 'hi';
+}
+
+export default function SocialDropdown({ iconList, selectedOption }: DropdownProps) {
+ const [name, setName] = useState('');
+
+ const handleNameChange = event => {
+ setName(event.target.value);
+ };
+
+ const handleAddItem = () => {
+ console.log('addItem');
+ // const { items, name } = this.state;
+ // this.setState({
+ // items: [...items, name || `New item ${index++}`],
+ // name: '',
+ // });
+ };
+
+
+ return (
+
+
+
+
+ );
+}
diff --git a/web/pages/components/main-layout.tsx b/web/pages/components/main-layout.tsx
index 359e38b83..44153ef7e 100644
--- a/web/pages/components/main-layout.tsx
+++ b/web/pages/components/main-layout.tsx
@@ -144,6 +144,9 @@ export default function MainLayout(props) {
Custom page content
+
+ Social links
+
Server Details
diff --git a/web/pages/config-social-links.tsx b/web/pages/config-social-links.tsx
new file mode 100644
index 000000000..9cfb4999e
--- /dev/null
+++ b/web/pages/config-social-links.tsx
@@ -0,0 +1,57 @@
+import React, { useState, useContext, useEffect } from 'react';
+import { Typography } from 'antd';
+import SocialDropdown from './components/config/social-icons-dropdown';
+import { fetchData, SOCIAL_PLATFORMS_LIST } from '../utils/apis';
+import { ServerStatusContext } from '../utils/server-status-context';
+
+const { Title } = Typography;
+
+
+// get icons
+
+export default function ConfigSocialLinks() {
+ const [availableIconsList, setAvailableIconsList] = useState([]);
+ const [currentSocialHandles, setCurrentSocialHandles] = useState([]);
+
+ const serverStatusData = useContext(ServerStatusContext);
+ const { serverConfig, setFieldInConfigState } = serverStatusData || {};
+
+ const { instanceDetails } = serverConfig;
+ const { socialHandles: initialSocialHandles } = instanceDetails;
+
+ const getAvailableIcons = async () => {
+ try {
+ const result = await fetchData(SOCIAL_PLATFORMS_LIST, { auth: false });
+ const list = Object.keys(result).map(item => ({
+ key: item,
+ ...result[item],
+ }));
+ console.log({result})
+ setAvailableIconsList(list);
+
+ } catch (error) {
+ console.log(error)
+ // do nothing
+ }
+ };
+
+ useEffect(() => {
+ getAvailableIcons();
+ }, []);
+
+ useEffect(() => {
+ setCurrentSocialHandles(initialSocialHandles);
+ }, [instanceDetails]);
+
+
+
+ return (
+
+
Social Links
+
Add all your social media handles and links to your other profiles here.
+
+
+
+ );
+}
+
diff --git a/web/pages/config-video.tsx b/web/pages/config-video.tsx
index 695996201..4222295e4 100644
--- a/web/pages/config-video.tsx
+++ b/web/pages/config-video.tsx
@@ -6,7 +6,7 @@ import VideoLatency from './components/config/video-latency';
const { Title } = Typography;
-export default function VideoConfig() {
+export default function ConfigVideoSettings() {
return (
Video configuration
diff --git a/web/styles/config.scss b/web/styles/config.scss
index 189b7c8ae..7f3e1c8d9 100644
--- a/web/styles/config.scss
+++ b/web/styles/config.scss
@@ -292,3 +292,49 @@
margin: auto;
display: inline-block;
}
+
+.social-option,
+.social-dropdown {
+ // .ant-select-selector,
+ // .ant-select-selection-search-input {
+ // height: 40px !important;
+ // }
+ .ant-select-item-option-content,
+ .ant-select-selection-item {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ padding: .25em;
+ line-height: normal;
+
+ .option-icon {
+ height: 1.5em;
+ width: 1.5em;
+ line-height: normal;
+ }
+ .option-label {
+ display: inline-block;
+ margin-left: 1em;
+ line-height: normal;
+ }
+ }
+}
+// .social-option {
+// .ant-select-item-option-content {
+// display: flex;
+// flex-direction: row;
+// justify-content: flex-start;
+// align-items: center;
+// padding: .25em;
+
+// .option-icon {
+// height: 1.75em;
+// width: 1.75em;
+// }
+// .option-label {
+// display: inline-block;
+// margin-left: 1em;
+// }
+// }
+// }
diff --git a/web/styles/globals.scss b/web/styles/globals.scss
index 6e0e7da15..1681c71b3 100644
--- a/web/styles/globals.scss
+++ b/web/styles/globals.scss
@@ -98,6 +98,16 @@ code {
font-size: 1.5em;
}
+
+
+.ant-select-dropdown {
+ background-color: #334;
+}
+.rc-virtual-list-scrollbar {
+ display: block !important;
+}
+
+
// markdown editor overrides
.rc-md-editor {
diff --git a/web/types/config-section.ts b/web/types/config-section.ts
index 417a3c2c8..d7144c78a 100644
--- a/web/types/config-section.ts
+++ b/web/types/config-section.ts
@@ -45,6 +45,7 @@ export interface ConfigInstanceDetailsFields {
logo: string;
name: string;
nsfw: boolean;
+ socialHandles: SocialHandleItem[],
streamTitle: string;
summary: string;
tags: string[];
@@ -54,6 +55,12 @@ export interface ConfigInstanceDetailsFields {
export type PRESET = 'fast' | 'faster' | 'veryfast' | 'superfast' | 'ultrafast';
+export interface SocialHandleItem {
+ icon: string;
+ platform: string;
+ key: string;
+}
+
export interface VideoVariant {
key?: number; // unique identifier generated on client side just for ant table rendering
encoderPreset: PRESET,
diff --git a/web/utils/apis.ts b/web/utils/apis.ts
index c6bd34593..1769f1c92 100644
--- a/web/utils/apis.ts
+++ b/web/utils/apis.ts
@@ -1,7 +1,7 @@
/* eslint-disable prefer-destructuring */
const ADMIN_USERNAME = process.env.NEXT_PUBLIC_ADMIN_USERNAME;
const ADMIN_STREAMKEY = process.env.NEXT_PUBLIC_ADMIN_STREAMKEY;
-const NEXT_PUBLIC_API_HOST = process.env.NEXT_PUBLIC_API_HOST;
+export const NEXT_PUBLIC_API_HOST = process.env.NEXT_PUBLIC_API_HOST;
const API_LOCATION = `${NEXT_PUBLIC_API_HOST}api/admin/`;
@@ -60,6 +60,9 @@ export const DELETE_WEBHOOK = `${API_LOCATION}webhooks/delete`;
// Create a single webhook
export const CREATE_WEBHOOK = `${API_LOCATION}webhooks/create`;
+// hard coded social icons list
+export const SOCIAL_PLATFORMS_LIST = `${NEXT_PUBLIC_API_HOST}api/socialplatforms`;
+
const GITHUB_RELEASE_URL = "https://api.github.com/repos/owncast/owncast/releases/latest";
diff --git a/web/utils/server-status-context.tsx b/web/utils/server-status-context.tsx
index bbb7be4ea..37480511f 100644
--- a/web/utils/server-status-context.tsx
+++ b/web/utils/server-status-context.tsx
@@ -14,6 +14,7 @@ export const initialServerConfigState: ConfigDetails = {
logo: '',
name: '',
nsfw: false,
+ socialHandles: [],
streamTitle: '',
summary: '',
tags: [],