From d1cc0dfd97627c09069616aa6db1e1ba9e02efb0 Mon Sep 17 00:00:00 2001 From: gingervitis Date: Tue, 19 Jan 2021 10:34:06 -0800 Subject: [PATCH] start on social links editing, wip --- web/pages/components/config/constants.tsx | 3 +- .../components/config/edit-social-links.tsx | 132 ++++++++++++++++++ .../config/social-icons-dropdown.tsx | 81 +++++++++++ web/pages/components/main-layout.tsx | 3 + web/pages/config-social-links.tsx | 57 ++++++++ web/pages/config-video.tsx | 2 +- web/styles/config.scss | 46 ++++++ web/styles/globals.scss | 10 ++ web/types/config-section.ts | 7 + web/utils/apis.ts | 5 +- web/utils/server-status-context.tsx | 1 + 11 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 web/pages/components/config/edit-social-links.tsx create mode 100644 web/pages/components/config/social-icons-dropdown.tsx create mode 100644 web/pages/config-social-links.tsx 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 ( +
+ + + Add item + +
+ + )} + > + {iconList.map(item => { + const { platform, icon, key } = item; + return ( + + + + + {platform} + + ); + }) + } + + + + ); +} 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: [],