diff --git a/web/pages/components/config/defaults.ts b/web/pages/components/config/constants.tsx
similarity index 81%
rename from web/pages/components/config/defaults.ts
rename to web/pages/components/config/constants.tsx
index e468595f8..b1bc93070 100644
--- a/web/pages/components/config/defaults.ts
+++ b/web/pages/components/config/constants.tsx
@@ -1,4 +1,6 @@
// DEFAULT VALUES
+import React from 'react';
+import { CheckCircleFilled, ExclamationCircleFilled } from '@ant-design/icons';
export const DEFAULT_NAME = 'Owncast User';
export const DEFAULT_TITLE = 'Owncast Server';
@@ -6,8 +8,22 @@ export const DEFAULT_SUMMARY = '';
export const TEXT_MAXLENGTH = 255;
+export const RESET_TIMEOUT = 3000;
+
+export const SUCCESS_STATES = {
+ success: {
+ icon: ,
+ message: 'Success!',
+ },
+ error: {
+ icon: ,
+ message: 'An error occurred.',
+ },
+};
+
// Creating this so that it'll be easier to change values in one place, rather than looking for places to change it in a sea of JSX.
+
// key is the input's `fieldName`
export const TEXTFIELD_DEFAULTS = {
@@ -95,5 +111,16 @@ export const TEXTFIELD_DEFAULTS = {
label: 'Server port',
tip: 'What port are you serving Owncast from? Default is :8080',
},
+
+ //
+ tags: {
+ apiPath: '/tags',
+ defaultValue: '',
+ maxLength: 24,
+ placeholder: 'Add a new tag',
+ configPath: 'instanceDetails',
+ label: '',
+ tip: '',
+ }
}
diff --git a/web/pages/components/config/form-textfield.tsx b/web/pages/components/config/form-textfield.tsx
index 18b01fcec..66d017e94 100644
--- a/web/pages/components/config/form-textfield.tsx
+++ b/web/pages/components/config/form-textfield.tsx
@@ -22,7 +22,7 @@ import { FormItemProps } from 'antd/es/form';
import { InfoCircleOutlined } from '@ant-design/icons';
-import { TEXTFIELD_DEFAULTS, TEXT_MAXLENGTH } from './defaults';
+import { TEXTFIELD_DEFAULTS, TEXT_MAXLENGTH, RESET_TIMEOUT } from './constants';
import { TextFieldProps } from '../../../types/config-section';
import { fetchData, SERVER_CONFIG_UPDATE_URL } from '../../../utils/apis';
@@ -84,7 +84,7 @@ export default function TextField(props: TextFieldProps) {
setSubmitStatus('error');
setSubmitStatusMessage(`There was an error: ${result.message}`);
}
- resetTimer = setTimeout(resetStates, 3000);
+ resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
};
const handleChange = e => {
@@ -139,7 +139,7 @@ export default function TextField(props: TextFieldProps) {
diff --git a/web/pages/components/config/tags.tsx b/web/pages/components/config/tags.tsx
index 7c5230a3b..fe97be1cf 100644
--- a/web/pages/components/config/tags.tsx
+++ b/web/pages/components/config/tags.tsx
@@ -1,33 +1,105 @@
/* eslint-disable react/no-array-index-key */
-import React, { useContext, useEffect } from 'react';
-import { Typography, Button, Tooltip } from 'antd';
-import { CloseCircleOutlined } from '@ant-design/icons';
+import React, { useContext, useState, useEffect } from 'react';
+import { Typography, Tag, Input } from 'antd';
+
import { ServerStatusContext } from '../../../utils/server-status-context';
+import { fetchData, SERVER_CONFIG_UPDATE_URL } from '../../../utils/apis';
+import { TEXTFIELD_DEFAULTS, RESET_TIMEOUT, SUCCESS_STATES } from './constants';
const { Title } = Typography;
-function Tag({ label }) {
- return (
-
- );
-}
-
export default function EditInstanceTags() {
+ const [newTagInput, setNewTagInput] = useState('');
+ const [submitStatus, setSubmitStatus] = useState(null);
+ const [submitDetails, setSubmitDetails] = useState('');
const serverStatusData = useContext(ServerStatusContext);
- const { serverConfig } = serverStatusData || {};
+ const { serverConfig, setConfigField } = serverStatusData || {};
const { instanceDetails } = serverConfig;
const { tags = [] } = instanceDetails;
- console.log(tags)
-
+
+ const {
+ apiPath,
+ maxLength,
+ placeholder,
+ configPath,
+ } = TEXTFIELD_DEFAULTS.tags || {};
+
+ let resetTimer = null;
+
+ useEffect(() => {
+ return () => {
+ clearTimeout(resetTimer);
+ }
+ }, []);
+
+ const resetStates = () => {
+ setSubmitStatus(null);
+ setSubmitDetails('');
+ resetTimer = null;
+ clearTimeout(resetTimer);
+ }
+
+ // posts all the tags at once as an array obj
+ const postUpdateToAPI = async (postValue: any) => {
+ // const result = await fetchData(`${SERVER_CONFIG_UPDATE_URL}${apiPath}`, {
+ // data: { value: postValue },
+ // method: 'POST',
+ // auth: true,
+ // });
+
+ const result = {
+ success: true,
+ message: 'success yay'
+ }
+ if (result.success) {
+ setConfigField({ fieldName: 'tags', value: postValue, path: configPath });
+ setSubmitStatus('success');
+ setSubmitDetails('Tags updated.');
+ setNewTagInput('');
+ } else {
+ setSubmitStatus('error');
+ setSubmitDetails(result.message);
+ }
+ resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
+ };
+
+ const handleInputChange = e => {
+ if (submitDetails !== '') {
+ setSubmitDetails('');
+ }
+ setNewTagInput(e.target.value);
+ };
+
+ // send to api and do stuff
+ const handleSubmitNewTag = () => {
+ resetStates();
+ const newTag = newTagInput.trim();
+ if (newTag === '') {
+ setSubmitDetails('Please enter a tag');
+ return;
+ }
+ if (tags.some(tag => tag.toLowerCase() === newTag.toLowerCase())) {
+ setSubmitDetails('This tag is already used!');
+ return;
+ }
+
+ const updatedTags = [...tags, newTag];
+ postUpdateToAPI(updatedTags);
+ };
+
+ const handleDeleteTag = index => {
+ resetStates();
+ const updatedTags = [...tags];
+ updatedTags.splice(index, 1);
+ postUpdateToAPI(updatedTags);
+ }
+
+ const {
+ icon: newStatusIcon = null,
+ message: newStatusMessage = '',
+ } = SUCCESS_STATES[submitStatus] || {};
+
return (
@@ -35,9 +107,31 @@ export default function EditInstanceTags() {
This is a great way to categorize your Owncast server on the Directory!
- {tags.map((tag, index) => )}
+ {tags.map((tag, index) => {
+ const handleClose = () => {
+ handleDeleteTag(index);
+ };
+ return (
+ {tag}
+ );
+ })}
+
+
+ {newStatusIcon} {newStatusMessage} {submitDetails}
+
+
+
+
);
}
-
diff --git a/web/styles/config.scss b/web/styles/config.scss
index a6d97be2d..d24b6761f 100644
--- a/web/styles/config.scss
+++ b/web/styles/config.scss
@@ -66,16 +66,51 @@
right: 0;
bottom: .5em;
}
-.tag {
- background-color: white;
- border-color: gray;
- .tag-delete {
- padding: 0 0 0 .3rem;
- margin-top: -4px;
+
+
+.tag-current-tags {
+ .ant-tag {
+ margin: .1rem;
+ font-size: .85rem;
+ border-radius: 10em;
+ padding: .25em 1em;
+ background-color: rgba(255,255,255,.5);
+
+ .ant-tag-close-icon {
+ transform: translateY(-1px);
+ margin-left: .3rem;
+ padding: 2px;
+ border-radius: 5rem;
+ border: 1px solid #eee;
+ &:hover {
+ border-color: #e03;
+ svg {
+ fill: black;
+ transition: fill .3s;
+ }
+ }
+ }
}
}
-.tag-current-tags {
- .tag {
- margin: .25rem ;
+.add-new-tag-section {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+
+ .new-tag-input {
+ width: 16em;
}
-}
\ No newline at end of file
+}
+.add-new-status {
+ margin: 1em 0;
+ min-height: 1.25em;
+ font-size: .75rem;
+ &.success {
+ color: var(--ant-success);
+ }
+ &.error {
+ color: var(--ant-error);
+ }
+}
+