mirror of
https://github.com/owncast/owncast.git
synced 2024-11-21 12:18:02 +03:00
Fix web project build errors
This commit is contained in:
parent
b66617961d
commit
72c01e1b9a
86 changed files with 863 additions and 813 deletions
2
.github/workflows/javascript-formatting.yml
vendored
2
.github/workflows/javascript-formatting.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
||||||
uses: creyD/prettier_action@v4.2
|
uses: creyD/prettier_action@v4.2
|
||||||
with:
|
with:
|
||||||
# This part is also where you can pass other options, for example:
|
# This part is also where you can pass other options, for example:
|
||||||
prettier_options: --write webroot/**/*.{js,md}
|
prettier_options: --write web/**/*.{js,ts,jsx,tsx,css,md}
|
||||||
working_directory: web
|
working_directory: web
|
||||||
only_changed: true
|
only_changed: true
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -40,6 +40,7 @@ module.exports = {
|
||||||
'@typescript-eslint/no-use-before-define': [1],
|
'@typescript-eslint/no-use-before-define': [1],
|
||||||
'no-shadow': 'off',
|
'no-shadow': 'off',
|
||||||
'@typescript-eslint/no-shadow': ['error'],
|
'@typescript-eslint/no-shadow': ['error'],
|
||||||
|
'no-restricted-exports': 'off',
|
||||||
'react/jsx-no-target-blank': [
|
'react/jsx-no-target-blank': [
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,4 +5,4 @@
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"arrowParens": "avoid"
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
|
|
24
web/.vscode/settings.json
vendored
24
web/.vscode/settings.json
vendored
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Owncast",
|
"Owncast",
|
||||||
"antd",
|
"antd",
|
||||||
"bitrates",
|
"bitrates",
|
||||||
"chartkick",
|
"chartkick",
|
||||||
"framerates",
|
"framerates",
|
||||||
"kbps",
|
"kbps",
|
||||||
"linkify",
|
"linkify",
|
||||||
"paypal",
|
"paypal",
|
||||||
"toggleswitch"
|
"toggleswitch"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,21 +8,21 @@ The Owncast web frontend is a [Next.js](https://nextjs.org/) project with [React
|
||||||
|
|
||||||
**First**, install the dependencies.
|
**First**, install the dependencies.
|
||||||
|
|
||||||
```npm install --include=dev```
|
`npm install --include=dev`
|
||||||
|
|
||||||
### Run the web project
|
### Run the web project
|
||||||
|
|
||||||
Make sure you're running an instance of Owncast on localhost:8080, as your copy of the admin will look to use that as the API.
|
Make sure you're running an instance of Owncast on localhost:8080, as your copy of the admin will look to use that as the API.
|
||||||
|
|
||||||
**Next**, start the web project with npm.
|
**Next**, start the web project with npm.
|
||||||
|
|
||||||
```npm run dev```
|
`npm run dev`
|
||||||
|
|
||||||
### Components and Styles
|
### Components and Styles
|
||||||
|
|
||||||
You can start the [Storybook](https://storybook.js.org/) UI for exploring, testing, and developing components by running:
|
You can start the [Storybook](https://storybook.js.org/) UI for exploring, testing, and developing components by running:
|
||||||
|
|
||||||
```npm run storybook```
|
`npm run storybook`
|
||||||
|
|
||||||
This allows for components to be made available without the need of the server to be running and changes to be made in
|
This allows for components to be made available without the need of the server to be running and changes to be made in
|
||||||
isolation.
|
isolation.
|
||||||
|
@ -63,4 +63,4 @@ We are currently experimenting with using [Storybook](https://storybook.js.org/)
|
||||||
|
|
||||||
To work with Storybook:
|
To work with Storybook:
|
||||||
|
|
||||||
```npm run storybook```
|
`npm run storybook`
|
||||||
|
|
|
@ -4,5 +4,6 @@ interface Props {
|
||||||
|
|
||||||
export default function CustomPageContent(props: Props) {
|
export default function CustomPageContent(props: Props) {
|
||||||
const { content } = props;
|
const { content } = props;
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
return <div dangerouslySetInnerHTML={{ __html: content }} />;
|
return <div dangerouslySetInnerHTML={{ __html: content }} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
interface Props {
|
export default function PageLogo() {
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PageLogo(props: Props) {
|
|
||||||
return <div>Pimary logo component goes here</div>;
|
return <div>Pimary logo component goes here</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { SocialLink } from '../interfaces/social-link.model';
|
import { SocialLink } from '../interfaces/social-link.model';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
links: SocialLink[];
|
links: SocialLink[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export default function SocialLinks(props: Props) {
|
export default function SocialLinks(props: Props) {
|
||||||
return <div>Social links component goes here</div>;
|
return <div>Social links component goes here</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
width: 20px;
|
width: 20px;
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Modal from '../ui/Modal/Modal';
|
import Modal from '../ui/Modal/Modal';
|
||||||
import { ExternalAction } from '../interfaces/external-action.interface';
|
import { ExternalAction } from '../../interfaces/external-action';
|
||||||
import s from './ActionButton.module.scss';
|
import s from './ActionButton.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
message: ChatMessage;
|
message: ChatMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export default function ChatSystemMessage(props: Props) {
|
export default function ChatSystemMessage(props: Props) {
|
||||||
return <div>Component goes here</div>;
|
return <div>Component goes here</div>;
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||||
import { ChatState } from '../../interfaces/application-state';
|
import { ChatState } from '../../interfaces/application-state';
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
export default function ChatModerationNotification(props: Props) {
|
export default function ChatModerationNotification(props: Props) {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable react/no-unused-prop-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/* eslint-disable react/no-unused-prop-types */
|
||||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export default function ChatTextField(props: Props) {
|
export default function ChatTextField(props: Props) {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [showEmojis, setShowEmojis] = useState(false);
|
const [showEmojis, setShowEmojis] = useState(false);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.root {
|
.root {
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
color: var(--black);
|
color: var(--black);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { SmileOutlined } from '@ant-design/icons';
|
import { SmileOutlined } from '@ant-design/icons';
|
||||||
import { Button, Popover } from 'antd';
|
import { Button, Popover } from 'antd';
|
||||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Transforms, createEditor, Node, BaseEditor, Text } from 'slate';
|
import { Transforms, createEditor, BaseEditor, Text } from 'slate';
|
||||||
import { Slate, Editable, withReact, ReactEditor } from 'slate-react';
|
import { Slate, Editable, withReact, ReactEditor } from 'slate-react';
|
||||||
import EmojiPicker from './EmojiPicker';
|
import EmojiPicker from './EmojiPicker';
|
||||||
import WebsocketService from '../../../services/websocket-service';
|
import WebsocketService from '../../../services/websocket-service';
|
||||||
import { websocketServiceAtom } from '../../stores/ClientConfigStore';
|
import { websocketServiceAtom } from '../../stores/ClientConfigStore';
|
||||||
import { MessageType } from '../../../interfaces/socket-events';
|
import { MessageType } from '../../../interfaces/socket-events';
|
||||||
import s from './ChatTextField.module.scss';
|
|
||||||
|
|
||||||
type CustomElement = { type: 'paragraph'; children: CustomText[] };
|
type CustomElement = { type: 'paragraph'; children: CustomText[] };
|
||||||
type CustomText = { text: string };
|
type CustomText = { text: string };
|
||||||
|
@ -25,24 +24,30 @@ interface Props {
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
const Image = ({ element }) => (
|
const Image = ({ element }) => (
|
||||||
<img
|
<img
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
src={element.url}
|
src={element.url}
|
||||||
alt="emoji"
|
alt="emoji"
|
||||||
style={{ display: 'inline', position: 'relative', width: '30px', bottom: '10px' }}
|
style={{ display: 'inline', position: 'relative', width: '30px', bottom: '10px' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const insertImage = (editor, url) => {
|
const insertImage = (editor, url) => {
|
||||||
const text = { text: '' };
|
// const text = { text: '' };
|
||||||
const image: ImageElement = { type: 'image', url, children: [text] };
|
// const image: ImageElement = { type: 'image', url, children: [text] };
|
||||||
Transforms.insertNodes(editor, image);
|
// Transforms.insertNodes(editor, image);
|
||||||
};
|
};
|
||||||
|
|
||||||
const withImages = editor => {
|
const withImages = editor => {
|
||||||
const { isVoid } = editor;
|
const { isVoid } = editor;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
editor.isVoid = element => (element.type === 'image' ? true : isVoid(element));
|
editor.isVoid = element => (element.type === 'image' ? true : isVoid(element));
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
editor.isInline = element => element.type === 'image';
|
editor.isInline = element => element.type === 'image';
|
||||||
|
|
||||||
return editor;
|
return editor;
|
||||||
|
@ -52,13 +57,13 @@ export type EmptyText = {
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ImageElement = {
|
// type ImageElement = {
|
||||||
type: 'image';
|
// type: 'image';
|
||||||
url: string;
|
// url: string;
|
||||||
children: EmptyText[];
|
// children: EmptyText[];
|
||||||
};
|
// };
|
||||||
|
|
||||||
const Element = props => {
|
const Element = (props: any) => {
|
||||||
const { attributes, children, element } = props;
|
const { attributes, children, element } = props;
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
|
@ -71,10 +76,10 @@ const Element = props => {
|
||||||
|
|
||||||
const serialize = node => {
|
const serialize = node => {
|
||||||
if (Text.isText(node)) {
|
if (Text.isText(node)) {
|
||||||
let string = node.text;
|
const string = node.text;
|
||||||
if (node.bold) {
|
// if (node.bold) {
|
||||||
string = `<strong>${string}</strong>`;
|
// string = `<strong>${string}</strong>`;
|
||||||
}
|
// }
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,8 +95,9 @@ const serialize = node => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export default function ChatTextField(props: Props) {
|
export default function ChatTextField(props: Props) {
|
||||||
const { value: originalValue } = props;
|
// const { value: originalValue } = props;
|
||||||
const [showEmojis, setShowEmojis] = useState(false);
|
const [showEmojis, setShowEmojis] = useState(false);
|
||||||
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||||
const [editor] = useState(() => withImages(withReact(createEditor())));
|
const [editor] = useState(() => withImages(withReact(createEditor())));
|
||||||
|
@ -113,7 +119,7 @@ export default function ChatTextField(props: Props) {
|
||||||
Transforms.delete(editor);
|
Transforms.delete(editor);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = e => {};
|
const handleChange = () => {};
|
||||||
|
|
||||||
const handleEmojiSelect = emoji => {
|
const handleEmojiSelect = emoji => {
|
||||||
console.log(emoji);
|
console.log(emoji);
|
||||||
|
@ -135,19 +141,12 @@ export default function ChatTextField(props: Props) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialValue = [
|
|
||||||
{
|
|
||||||
type: 'paragraph',
|
|
||||||
children: [{ text: originalValue }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Slate editor={editor} value={initialValue} onChange={handleChange}>
|
<Slate editor={editor} value={[]} onChange={handleChange}>
|
||||||
<Editable
|
<Editable
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
renderElement={props => <Element {...props} />}
|
renderElement={p => <Element {...p} />}
|
||||||
placeholder="Chat message goes here..."
|
placeholder="Chat message goes here..."
|
||||||
/>
|
/>
|
||||||
</Slate>
|
</Slate>
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
import data from '@emoji-mart/data';
|
// import data from '@emoji-mart/data';
|
||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
export default function EmojiPicker(props) {
|
interface Props {
|
||||||
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
|
onEmojiSelect: (emoji: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export default function EmojiPicker(props: Props) {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
|
|
||||||
// TODO: Pull this custom emoji data in from the emoji API.
|
// TODO: Pull this custom emoji data in from the emoji API.
|
||||||
const custom = [
|
// const custom = [
|
||||||
{
|
// {
|
||||||
emojis: [
|
// emojis: [
|
||||||
{
|
// {
|
||||||
id: 'party_parrot',
|
// id: 'party_parrot',
|
||||||
name: 'Party Parrot',
|
// name: 'Party Parrot',
|
||||||
keywords: ['dance', 'dancing'],
|
// keywords: ['dance', 'dancing'],
|
||||||
skins: [{ src: 'https://watch.owncast.online/img/emoji/bluntparrot.gif' }],
|
// skins: [{ src: 'https://watch.owncast.online/img/emoji/bluntparrot.gif' }],
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
},
|
// },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
// TODO: Fix the emoji picker from throwing errors.
|
// TODO: Fix the emoji picker from throwing errors.
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
|
|
|
@ -7,7 +7,9 @@ interface Props {
|
||||||
|
|
||||||
export default function ChatUserMessage(props: Props) {
|
export default function ChatUserMessage(props: Props) {
|
||||||
const { message, showModeratorMenu } = props;
|
const { message, showModeratorMenu } = props;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { body, user, timestamp } = message;
|
const { body, user, timestamp } = message;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { displayName, displayColor } = user;
|
const { displayName, displayColor } = user;
|
||||||
|
|
||||||
// TODO: Convert displayColor (a hue) to a usable color.
|
// TODO: Convert displayColor (a hue) to a usable color.
|
||||||
|
|
|
@ -101,45 +101,6 @@ export function convertToText(str = '') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
You would call this when a user pastes from
|
|
||||||
the clipboard into a `contenteditable` area.
|
|
||||||
*/
|
|
||||||
export function convertOnPaste(event = { preventDefault() {} }, emojiList) {
|
|
||||||
// Prevent paste.
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// Set later.
|
|
||||||
let value = '';
|
|
||||||
|
|
||||||
// Does method exist?
|
|
||||||
const hasEventClipboard = !!(
|
|
||||||
event.clipboardData &&
|
|
||||||
typeof event.clipboardData === 'object' &&
|
|
||||||
typeof event.clipboardData.getData === 'function'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get clipboard data?
|
|
||||||
if (hasEventClipboard) {
|
|
||||||
value = event.clipboardData.getData('text/plain');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert into temp `<textarea>`, read back out.
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
textarea.innerHTML = value;
|
|
||||||
value = textarea.innerText;
|
|
||||||
|
|
||||||
// Clean up text.
|
|
||||||
value = convertToText(value);
|
|
||||||
|
|
||||||
const HTML = emojify(value, emojiList);
|
|
||||||
|
|
||||||
// Insert text.
|
|
||||||
if (typeof document.execCommand === 'function') {
|
|
||||||
document.execCommand('insertHTML', false, HTML);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createEmojiMarkup(data, isCustom) {
|
export function createEmojiMarkup(data, isCustom) {
|
||||||
const emojiUrl = isCustom ? data.emoji : data.url;
|
const emojiUrl = isCustom ? data.emoji : data.url;
|
||||||
const emojiName = (
|
const emojiName = (
|
||||||
|
@ -156,6 +117,7 @@ export function trimNbsp(html) {
|
||||||
export function emojify(HTML, emojiList) {
|
export function emojify(HTML, emojiList) {
|
||||||
const textValue = convertToText(HTML);
|
const textValue = convertToText(HTML);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let lastPos = textValue.length; lastPos >= 0; lastPos--) {
|
for (let lastPos = textValue.length; lastPos >= 0; lastPos--) {
|
||||||
const endPos = textValue.lastIndexOf(':', lastPos);
|
const endPos = textValue.lastIndexOf(':', lastPos);
|
||||||
if (endPos <= 0) {
|
if (endPos <= 0) {
|
||||||
|
@ -170,8 +132,9 @@ export function emojify(HTML, emojiList) {
|
||||||
emojiItem => emojiItem.name.toLowerCase() === typedEmoji.toLowerCase(),
|
emojiItem => emojiItem.name.toLowerCase() === typedEmoji.toLowerCase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (emojiIndex != -1) {
|
if (emojiIndex !== -1) {
|
||||||
const emojiImgElement = createEmojiMarkup(emojiList[emojiIndex], true);
|
const emojiImgElement = createEmojiMarkup(emojiList[emojiIndex], true);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
HTML = HTML.replace(`:${typedEmoji}:`, emojiImgElement);
|
HTML = HTML.replace(`:${typedEmoji}:`, emojiImgElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import s from './Logo.module.scss'
|
import s from './Logo.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
variant: 'simple' | 'contrast'
|
variant: 'simple' | 'contrast';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Logo({variant = 'simple'}: Props) {
|
export default function Logo({ variant = 'simple' }: Props) {
|
||||||
const rootClassName = cn(
|
const rootClassName = cn(s.root, {
|
||||||
s.root,
|
[s.simple]: variant === 'simple',
|
||||||
{
|
[s.contrast]: variant === 'contrast',
|
||||||
[s.simple]: variant === 'simple',
|
});
|
||||||
[s.contrast]: variant === 'contrast',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={rootClassName}>
|
<div className={rootClassName}>
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { ChatState, ChatVisibilityState } from '../../../interfaces/application-
|
||||||
import s from './UserDropdown.module.scss';
|
import s from './UserDropdown.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
username?: string;
|
username: string;
|
||||||
chatState?: ChatState;
|
chatState: ChatState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserDropdown({ username = 'test-user', chatState }: Props) {
|
export default function UserDropdown({ username = 'test-user', chatState }: Props) {
|
||||||
|
@ -44,11 +44,6 @@ export default function UserDropdown({ username = 'test-user', chatState }: Prop
|
||||||
<DownOutlined />
|
<DownOutlined />
|
||||||
</Space>
|
</Space>
|
||||||
</Button>
|
</Button>
|
||||||
{/*
|
|
||||||
<button type="button" className="ant-dropdown-link" onClick={e => e.preventDefault()}>
|
|
||||||
{username} <DownOutlined />
|
|
||||||
</button>
|
|
||||||
*/}
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export { default } from './UserDropdown'
|
export { default } from './UserDropdown';
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
export { default as UserDropdown } from './UserDropdown'
|
export { default as UserDropdown } from './UserDropdown';
|
||||||
export { default as OwncastLogo } from './Logo'
|
export { default as OwncastLogo } from './Logo';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { AppProps } from 'next/app';
|
import { AppProps } from 'next/app';
|
||||||
import ServerStatusProvider from '../../utils/server-status-context';
|
import ServerStatusProvider from '../../utils/server-status-context';
|
||||||
import AlertMessageProvider from '../../utils/alert-message-context';
|
import AlertMessageProvider from '../../utils/alert-message-context';
|
||||||
import MainLayout from '../../components/main-layout';
|
import MainLayout from '../main-layout';
|
||||||
|
|
||||||
function AdminLayout({ Component, pageProps }: AppProps) {
|
function AdminLayout({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
|
@ -130,7 +130,7 @@ export default function MainLayout(props) {
|
||||||
<Sider width={240} className="side-nav">
|
<Sider width={240} className="side-nav">
|
||||||
<h1 className="owncast-title">
|
<h1 className="owncast-title">
|
||||||
<span className="logo-container">
|
<span className="logo-container">
|
||||||
<OwncastLogo />
|
<OwncastLogo variant="simple" />
|
||||||
</span>
|
</span>
|
||||||
<span className="title-label">Owncast Admin</span>
|
<span className="title-label">Owncast Admin</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
export default function AuthModal(props: Props) {
|
export default function AuthModal(props: Props) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
export default function BrowserNotifyModal(props: Props) {
|
export default function BrowserNotifyModal(props: Props) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
export default function FediAuthModal(props: Props) {
|
export default function FediAuthModal(props: Props) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
export default function FollowModal(props: Props) {
|
export default function FollowModal(props: Props) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
export default function IndieAuthModal(props: Props) {
|
export default function IndieAuthModal(props: Props) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Card, Col, Row, Typography } from 'antd';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import LogTable from './log-table';
|
import LogTable from './log-table';
|
||||||
import OwncastLogo from './logo';
|
import OwncastLogo from './common/Logo/Logo';
|
||||||
import NewsFeed from './news-feed';
|
import NewsFeed from './news-feed';
|
||||||
import { ConfigDetails } from '../types/config-section';
|
import { ConfigDetails } from '../types/config-section';
|
||||||
import { ServerStatusContext } from '../utils/server-status-context';
|
import { ServerStatusContext } from '../utils/server-status-context';
|
||||||
|
@ -125,7 +125,7 @@ export default function Offline({ logs = [], config }: OfflineProps) {
|
||||||
<Col span={12} offset={6}>
|
<Col span={12} offset={6}>
|
||||||
<div className="offline-intro">
|
<div className="offline-intro">
|
||||||
<span className="logo">
|
<span className="logo">
|
||||||
<OwncastLogo />
|
<OwncastLogo variant="simple" />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<Title level={2}>No stream is active</Title>
|
<Title level={2}>No stream is active</Title>
|
||||||
|
|
|
@ -6,7 +6,6 @@ import ClientConfigService from '../../services/client-config-service';
|
||||||
import ChatService from '../../services/chat-service';
|
import ChatService from '../../services/chat-service';
|
||||||
import WebsocketService from '../../services/websocket-service';
|
import WebsocketService from '../../services/websocket-service';
|
||||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||||
import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
|
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
ChatState,
|
ChatState,
|
||||||
|
@ -16,10 +15,10 @@ import {
|
||||||
getChatVisibilityState,
|
getChatVisibilityState,
|
||||||
} from '../../interfaces/application-state';
|
} from '../../interfaces/application-state';
|
||||||
import {
|
import {
|
||||||
SocketEvent,
|
|
||||||
ConnectedClientInfoEvent,
|
ConnectedClientInfoEvent,
|
||||||
MessageType,
|
MessageType,
|
||||||
ChatEvent,
|
ChatEvent,
|
||||||
|
SocketEvent,
|
||||||
} from '../../interfaces/socket-events';
|
} from '../../interfaces/socket-events';
|
||||||
import handleConnectedClientInfoMessage from './eventhandlers/connectedclientinfo';
|
import handleConnectedClientInfoMessage from './eventhandlers/connectedclientinfo';
|
||||||
import handleChatMessage from './eventhandlers/handleChatMessage';
|
import handleChatMessage from './eventhandlers/handleChatMessage';
|
||||||
|
@ -77,10 +76,8 @@ export function ClientConfigStore() {
|
||||||
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
|
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
|
||||||
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
||||||
const [appState, setAppState] = useRecoilState<AppState>(appStateAtom);
|
const [appState, setAppState] = useRecoilState<AppState>(appStateAtom);
|
||||||
const [videoState, setVideoState] = useRecoilState<VideoState>(videoStateAtom);
|
|
||||||
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
||||||
const [websocketService, setWebsocketService] =
|
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
|
||||||
useRecoilState<WebsocketService>(websocketServiceAtom);
|
|
||||||
|
|
||||||
let ws: WebsocketService;
|
let ws: WebsocketService;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ConnectedClientInfoEvent, SocketEvent } from '../../../interfaces/socket-events';
|
import { ConnectedClientInfoEvent } from '../../../interfaces/socket-events';
|
||||||
|
|
||||||
export default function handleConnectedClientInfoMessage(message: ConnectedClientInfoEvent) {
|
export default function handleConnectedClientInfoMessage(message: ConnectedClientInfoEvent) {
|
||||||
console.log('connected client', message);
|
console.log('connected client', message);
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
.root {
|
.root {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileChat {
|
.mobileChat {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(50vh - var(--header-h));
|
height: calc(50vh - var(--header-h));
|
||||||
}
|
}
|
||||||
|
|
||||||
.leftCol {
|
.leftCol {
|
||||||
display: grid;
|
display: grid;
|
||||||
// -64px, which is the header
|
// -64px, which is the header
|
||||||
grid-template-rows: 50vh calc(50vh - var(--header-h));
|
grid-template-rows: 50vh calc(50vh - var(--header-h));
|
||||||
}
|
}
|
||||||
.lowerRow {
|
.lowerRow {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr var(--header-h);
|
grid-template-rows: 1fr var(--header-h);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageContentSection {
|
.pageContentSection {
|
||||||
|
@ -32,10 +32,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.mobileChat {
|
.mobileChat {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.root[data-columns='2'] {
|
.root[data-columns='2'] {
|
||||||
grid-template-columns: 1fr var(--chat-w);
|
grid-template-columns: 1fr var(--chat-w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Layout, Button, Col, Tabs } from 'antd';
|
import { Layout, Button, Tabs } from 'antd';
|
||||||
import Grid from 'antd/lib/card/Grid';
|
|
||||||
import {
|
import {
|
||||||
chatVisibilityAtom,
|
chatVisibilityAtom,
|
||||||
clientConfigStateAtom,
|
clientConfigStateAtom,
|
||||||
|
@ -23,6 +22,7 @@ import ActionButtonRow from '../../action-buttons/ActionButtonRow';
|
||||||
import ActionButton from '../../action-buttons/ActionButton';
|
import ActionButton from '../../action-buttons/ActionButton';
|
||||||
import Statusbar from '../Statusbar/Statusbar';
|
import Statusbar from '../Statusbar/Statusbar';
|
||||||
import { ServerStatus } from '../../../interfaces/server-status.model';
|
import { ServerStatus } from '../../../interfaces/server-status.model';
|
||||||
|
import { Follower } from '../../../interfaces/follower';
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
@ -34,8 +34,8 @@ export default function ContentComponent() {
|
||||||
const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
|
const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
|
||||||
const chatState = useRecoilValue<ChatState>(chatStateAtom);
|
const chatState = useRecoilValue<ChatState>(chatStateAtom);
|
||||||
|
|
||||||
const { extraPageContent } = clientConfig;
|
const { extraPageContent, version } = clientConfig;
|
||||||
const { online, viewerCount, lastConnectTime, lastDisconnectTime, streamTitle } = status;
|
const { online, viewerCount, lastConnectTime, lastDisconnectTime } = status;
|
||||||
|
|
||||||
const followers: Follower[] = [];
|
const followers: Follower[] = [];
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ export default function ContentComponent() {
|
||||||
<ChatTextField />
|
<ChatTextField />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Footer />
|
<Footer version={version} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{chatOpen && <Sidebar />}
|
{chatOpen && <Sidebar />}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export { default } from "./Content"
|
export { default } from './Content';
|
||||||
|
|
|
@ -54,11 +54,11 @@ export default function CrossfadeImage({
|
||||||
return (
|
return (
|
||||||
<span style={spanStyle}>
|
<span style={spanStyle}>
|
||||||
{[...srcs, nextSrc].map(
|
{[...srcs, nextSrc].map(
|
||||||
(src, index) =>
|
(singleSrc, index) =>
|
||||||
src !== '' && (
|
singleSrc !== '' && (
|
||||||
<img
|
<img
|
||||||
key={(key + index) % 3}
|
key={singleSrc}
|
||||||
src={src}
|
src={singleSrc}
|
||||||
alt=""
|
alt=""
|
||||||
style={imgStyles[index]}
|
style={imgStyles[index]}
|
||||||
onLoad={index === 2 ? onLoadImg : undefined}
|
onLoad={index === 2 ? onLoadImg : undefined}
|
||||||
|
|
|
@ -2,7 +2,11 @@ import { Layout } from 'antd';
|
||||||
|
|
||||||
const { Footer } = Layout;
|
const { Footer } = Layout;
|
||||||
|
|
||||||
export default function FooterComponent(props) {
|
interface Props {
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FooterComponent(props: Props) {
|
||||||
const { version } = props;
|
const { version } = props;
|
||||||
|
|
||||||
return <Footer style={{ textAlign: 'center', height: '64px' }}>Footer: Owncast {version}</Footer>;
|
return <Footer style={{ textAlign: 'center', height: '64px' }}>Footer: Owncast {version}</Footer>;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: .5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Layout } from 'antd';
|
import { Layout } from 'antd';
|
||||||
|
import { ChatState } from '../../../interfaces/application-state';
|
||||||
import { OwncastLogo, UserDropdown } from '../../common';
|
import { OwncastLogo, UserDropdown } from '../../common';
|
||||||
import s from './Header.module.scss';
|
import s from './Header.module.scss';
|
||||||
|
|
||||||
|
@ -12,10 +13,10 @@ export default function HeaderComponent({ name = 'Your stream title' }: Props) {
|
||||||
return (
|
return (
|
||||||
<Header className={`${s.header}`}>
|
<Header className={`${s.header}`}>
|
||||||
<div className={`${s.logo}`}>
|
<div className={`${s.logo}`}>
|
||||||
<OwncastLogo variant='contrast'/>
|
<OwncastLogo variant="contrast" />
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
</div>
|
</div>
|
||||||
<UserDropdown />
|
<UserDropdown username="fillmein" chatState={ChatState.Available} />
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,4 +8,4 @@
|
||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 2vw;
|
padding: 2vw;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@ export default function Modal(props: Props) {
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
||||||
allowpaymentrequest="true"
|
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
onLoad={() => setLoading(false)}
|
onLoad={() => setLoading(false)}
|
||||||
|
|
|
@ -8,5 +8,3 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,4 +7,4 @@
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default function Statusbar(props: Props) {
|
||||||
const duration = makeDurationString(new Date(lastConnectTime));
|
const duration = makeDurationString(new Date(lastConnectTime));
|
||||||
onlineMessage = online ? `Live for ${duration}` : 'Offline';
|
onlineMessage = online ? `Live for ${duration}` : 'Offline';
|
||||||
rightSideMessage = `${viewerCount > 0 ? `${viewerCount}` : 'No'} ${
|
rightSideMessage = `${viewerCount > 0 ? `${viewerCount}` : 'No'} ${
|
||||||
viewerCount == 1 ? 'viewer' : 'viewers'
|
viewerCount === 1 ? 'viewer' : 'viewers'
|
||||||
}`;
|
}`;
|
||||||
} else {
|
} else {
|
||||||
onlineMessage = 'Offline';
|
onlineMessage = 'Offline';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export { default as Header } from './Header/index'
|
export { default as Header } from './Header/index';
|
||||||
export { default as Sidebar } from './Sidebar/index'
|
export { default as Sidebar } from './Sidebar/index';
|
||||||
export { default as Footer } from './Footer/index'
|
export { default as Footer } from './Footer/index';
|
||||||
export { default as Content } from './Content/index'
|
export { default as Content } from './Content/index';
|
||||||
|
|
|
@ -107,11 +107,7 @@ export default function OwncastPlayer(props: Props) {
|
||||||
<div style={{ display: 'grid' }}>
|
<div style={{ display: 'grid' }}>
|
||||||
{online && (
|
{online && (
|
||||||
<div style={{ gridColumn: 1, gridRow: 1 }}>
|
<div style={{ gridColumn: 1, gridRow: 1 }}>
|
||||||
<VideoJS
|
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} />
|
||||||
style={{ gridColumn: 1, gridRow: 1 }}
|
|
||||||
options={videoJsOptions}
|
|
||||||
onReady={handlePlayerReady}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ gridColumn: 1, gridRow: 1 }}>
|
<div style={{ gridColumn: 1, gridRow: 1 }}>
|
||||||
|
|
|
@ -6,4 +6,4 @@
|
||||||
.vjs-big-play-centered .vjs-big-play-button {
|
.vjs-big-play-centered .vjs-big-play-button {
|
||||||
z-index: 99999 !important;
|
z-index: 99999 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
background-color: black;
|
background-color: black;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,12 @@ require('video.js/dist/video-js.css');
|
||||||
// TODO: Restore volume that was saved in local storage.
|
// TODO: Restore volume that was saved in local storage.
|
||||||
// import { getLocalStorage, setLocalStorage } from '../../utils/helpers.js';
|
// import { getLocalStorage, setLocalStorage } from '../../utils/helpers.js';
|
||||||
// import { PLAYER_VOLUME, URL_STREAM } from '../../utils/constants.js';
|
// import { PLAYER_VOLUME, URL_STREAM } from '../../utils/constants.js';
|
||||||
|
interface Props {
|
||||||
|
options: any;
|
||||||
|
onReady: (player: videojs.Player) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export function VideoJS(props) {
|
export function VideoJS(props: Props) {
|
||||||
const videoRef = React.useRef(null);
|
const videoRef = React.useRef(null);
|
||||||
const playerRef = React.useRef(null);
|
const playerRef = React.useRef(null);
|
||||||
const { options, onReady } = props;
|
const { options, onReady } = props;
|
||||||
|
@ -18,11 +22,10 @@ export function VideoJS(props) {
|
||||||
if (!playerRef.current) {
|
if (!playerRef.current) {
|
||||||
const videoElement = videoRef.current;
|
const videoElement = videoRef.current;
|
||||||
|
|
||||||
// if (!videoElement) return;
|
// eslint-disable-next-line no-multi-assign
|
||||||
|
|
||||||
const player = (playerRef.current = videojs(videoElement, options, () => {
|
const player = (playerRef.current = videojs(videoElement, options, () => {
|
||||||
player.log('player is ready');
|
player.log('player is ready');
|
||||||
onReady && onReady(player);
|
return onReady && onReady(player);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// TODO: Add airplay support, video settings menu, latency compensator, etc.
|
// TODO: Add airplay support, video settings menu, latency compensator, etc.
|
||||||
|
@ -48,6 +51,7 @@ export function VideoJS(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-vjs-player>
|
<div data-vjs-player>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||||
<video ref={videoRef} className={`video-js vjs-big-play-centered ${s.player}`} />
|
<video ref={videoRef} className={`video-js vjs-big-play-centered ${s.player}`} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
# Tips for creating a new Admin form
|
# Tips for creating a new Admin form
|
||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
|
|
||||||
- Give your page or form a title. Feel free to use Ant Design's `<Title>` component.
|
- Give your page or form a title. Feel free to use Ant Design's `<Title>` component.
|
||||||
- Give your form a description inside of a `<p className="description" />` tag.
|
- Give your form a description inside of a `<p className="description" />` tag.
|
||||||
|
|
||||||
- Use some Ant Design `Row` and `Col`'s to layout your forms if you want to spread them out into responsive columns. If you use an `<Row>`s, be sure to use `<Col>`s with them too!
|
- Use some Ant Design `Row` and `Col`'s to layout your forms if you want to spread them out into responsive columns. If you use an `<Row>`s, be sure to use `<Col>`s with them too!
|
||||||
|
|
||||||
- Use the `form-module` CSS class if you want to add a visual separation to a grouping of items.
|
- Use the `form-module` CSS class if you want to add a visual separation to a grouping of items.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Form fields
|
### Form fields
|
||||||
|
|
||||||
- Feel free to use the pre-styled `<TextField>` text form field or the `<ToggleSwitch>` compnent, in a group of form fields together. These have been styled and laid out to match each other.
|
- Feel free to use the pre-styled `<TextField>` text form field or the `<ToggleSwitch>` compnent, in a group of form fields together. These have been styled and laid out to match each other.
|
||||||
|
|
||||||
- `Slider`'s - If your form uses an Ant Slider component, follow this recommended markup of CSS classes to maintain a consistent look and feel to other Sliders in the app.
|
- `Slider`'s - If your form uses an Ant Slider component, follow this recommended markup of CSS classes to maintain a consistent look and feel to other Sliders in the app.
|
||||||
```
|
```
|
||||||
<div className="segment-slider-container">
|
<div className="segment-slider-container">
|
||||||
<Slider ...props />
|
<Slider ...props />
|
||||||
|
@ -22,24 +22,28 @@
|
||||||
```
|
```
|
||||||
|
|
||||||
### Submit Statuses
|
### Submit Statuses
|
||||||
- It would be nice to display indicators of success/warnings to let users know if something has been successfully updated on the server. It has a lot of steps (sorry, but it could probably be optimized), but it'll provide a consistent way to display messaging.
|
|
||||||
|
- It would be nice to display indicators of success/warnings to let users know if something has been successfully updated on the server. It has a lot of steps (sorry, but it could probably be optimized), but it'll provide a consistent way to display messaging.
|
||||||
|
|
||||||
- See `reset-yp.tsx` for an example of using `submitStatus` with `useState()` and the `<FormStatusIndicator>` component to achieve this.
|
- See `reset-yp.tsx` for an example of using `submitStatus` with `useState()` and the `<FormStatusIndicator>` component to achieve this.
|
||||||
|
|
||||||
### Styling
|
### Styling
|
||||||
- This admin site chooses to have a generally Dark color palette, but with colors that are different from Ant design's _dark_ stylesheet, so that style sheet is not included. This results in a very large `ant-overrides.scss` file to reset colors on frequently used Ant components in the system. If you find yourself a new Ant Component that has not yet been used in this app, feel free to add a reset style for that component to the overrides stylesheet.
|
|
||||||
|
- This admin site chooses to have a generally Dark color palette, but with colors that are different from Ant design's _dark_ stylesheet, so that style sheet is not included. This results in a very large `ant-overrides.scss` file to reset colors on frequently used Ant components in the system. If you find yourself a new Ant Component that has not yet been used in this app, feel free to add a reset style for that component to the overrides stylesheet.
|
||||||
|
|
||||||
- Take a look at `variables.scss` CSS file if you want to give some elements custom css colors.
|
- Take a look at `variables.scss` CSS file if you want to give some elements custom css colors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
---
|
---
|
||||||
---
|
|
||||||
# Creating Admin forms the Config section
|
# Creating Admin forms the Config section
|
||||||
|
|
||||||
First things first..
|
First things first..
|
||||||
|
|
||||||
## General Config data flow in this React app
|
## General Config data flow in this React app
|
||||||
|
|
||||||
- When the Admin app loads, the `ServerStatusContext` (in addition to checking server `/status` on a timer) makes a call to the `/serverconfig` API to get your config details. This data will be stored as **`serverConfig`** in app state, and _provided_ to the app via `useContext` hook.
|
- When the Admin app loads, the `ServerStatusContext` (in addition to checking server `/status` on a timer) makes a call to the `/serverconfig` API to get your config details. This data will be stored as **`serverConfig`** in app state, and _provided_ to the app via `useContext` hook.
|
||||||
|
|
||||||
- The `serverConfig` in state is be the central source of data that pre-populates the forms.
|
- The `serverConfig` in state is be the central source of data that pre-populates the forms.
|
||||||
|
|
||||||
|
@ -47,39 +51,42 @@ First things first..
|
||||||
|
|
||||||
- After you have updated a config value in a form field, and successfully submitted it through its endpoint, you should call `setFieldInConfigState` to update the global state with the new value.
|
- After you have updated a config value in a form field, and successfully submitted it through its endpoint, you should call `setFieldInConfigState` to update the global state with the new value.
|
||||||
|
|
||||||
|
|
||||||
## Suggested Config Form Flow
|
## Suggested Config Form Flow
|
||||||
- *NOTE: Each top field of the serverConfig has its own API update endpoint.*
|
|
||||||
|
|
||||||
|
- _NOTE: Each top field of the serverConfig has its own API update endpoint._
|
||||||
|
|
||||||
There many steps here, but they are highly suggested to ensure that Config values are updated and displayed properly throughout the entire admin form.
|
There many steps here, but they are highly suggested to ensure that Config values are updated and displayed properly throughout the entire admin form.
|
||||||
|
|
||||||
For each form input (or group of inputs) you make, you should:
|
For each form input (or group of inputs) you make, you should:
|
||||||
1. Get the field values that you want out of `serverConfig` from ServerStatusContext with `useContext`.
|
|
||||||
2. Next we'll have to put these field values of interest into a `useState` in each grouping. This will help you edit the form.
|
1. Get the field values that you want out of `serverConfig` from ServerStatusContext with `useContext`.
|
||||||
3. Because ths config data is populated asynchronously, Use a `useEffect` to check when that data has arrived before putting it into state.
|
2. Next we'll have to put these field values of interest into a `useState` in each grouping. This will help you edit the form.
|
||||||
4. You will be using the state's value to populate the `defaultValue` and the `value` props of each Ant input component (`Input`, `Toggle`, `Switch`, `Select`, `Slider` are currently used).
|
3. Because ths config data is populated asynchronously, Use a `useEffect` to check when that data has arrived before putting it into state.
|
||||||
5. When an `onChange` event fires for each type of input component, you will update the local state of each page with the changed value.
|
4. You will be using the state's value to populate the `defaultValue` and the `value` props of each Ant input component (`Input`, `Toggle`, `Switch`, `Select`, `Slider` are currently used).
|
||||||
6. Depending on the form, an `onChange` of the input component, or a subsequent `onClick` of a submit button will take the value from local state and POST the field's API.
|
5. When an `onChange` event fires for each type of input component, you will update the local state of each page with the changed value.
|
||||||
7. `onSuccess` of the post, you should update the global app state with the new value.
|
6. Depending on the form, an `onChange` of the input component, or a subsequent `onClick` of a submit button will take the value from local state and POST the field's API.
|
||||||
|
7. `onSuccess` of the post, you should update the global app state with the new value.
|
||||||
|
|
||||||
There are also a variety of other local states to manage the display of error/success messaging.
|
There are also a variety of other local states to manage the display of error/success messaging.
|
||||||
|
|
||||||
- It is recommended that you use `form-textfield-with-submit` and `form-toggleswitch`(with `useSubmit=true`) Components to edit Config fields.
|
- It is recommended that you use `form-textfield-with-submit` and `form-toggleswitch`(with `useSubmit=true`) Components to edit Config fields.
|
||||||
|
|
||||||
Examples of Config form groups where individual form fields submitting to the update API include:
|
Examples of Config form groups where individual form fields submitting to the update API include:
|
||||||
|
|
||||||
- `edit-instance-details.tsx`
|
- `edit-instance-details.tsx`
|
||||||
- `edit-server-details.tsx`
|
- `edit-server-details.tsx`
|
||||||
|
|
||||||
Examples of Config form groups where there is 1 submit button for the entire group include:
|
Examples of Config form groups where there is 1 submit button for the entire group include:
|
||||||
|
|
||||||
- `edit-storage.tsx`
|
- `edit-storage.tsx`
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Notes about `form-textfield-with-submit` and `form-togglefield` (with useSubmit=true)
|
#### Notes about `form-textfield-with-submit` and `form-togglefield` (with useSubmit=true)
|
||||||
|
|
||||||
- The text field is intentionally designed to make it difficult for the user to submit bad data.
|
- The text field is intentionally designed to make it difficult for the user to submit bad data.
|
||||||
- If you make a change on a field, a Submit buttton will show up that you have to click to update. That will be the only way you can update it.
|
- If you make a change on a field, a Submit buttton will show up that you have to click to update. That will be the only way you can update it.
|
||||||
- If you clear out a field that is marked as Required, then exit/blur the field, it will repopulate with its original value.
|
- If you clear out a field that is marked as Required, then exit/blur the field, it will repopulate with its original value.
|
||||||
|
|
||||||
- Both of these elements are specifically meant to be used with updating `serverConfig` fields, since each field requires its own endpoint.
|
- Both of these elements are specifically meant to be used with updating `serverConfig` fields, since each field requires its own endpoint.
|
||||||
|
|
||||||
|
@ -88,4 +95,3 @@ Examples of Config form groups where there is 1 submit button for the entire gro
|
||||||
- (currently undergoing re-styling and TS cleanup)
|
- (currently undergoing re-styling and TS cleanup)
|
||||||
|
|
||||||
- NOTE: you don't have to use these components. Some form groups may require a customized UX flow where you're better off using the Ant components straight up.
|
- NOTE: you don't have to use these components. Some form groups may require a customized UX flow where you're better off using the Ant components straight up.
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export interface ClientConfig {
|
export interface ClientConfig {
|
||||||
name: string;
|
name: string;
|
||||||
|
title?: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
logo: string;
|
logo: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
|
|
@ -3,4 +3,6 @@ export interface ExternalAction {
|
||||||
description?: string;
|
description?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
icon?: string;
|
||||||
|
openExternally?: boolean;
|
||||||
}
|
}
|
||||||
|
|
693
web/package-lock.json
generated
693
web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -15,7 +15,7 @@
|
||||||
"@ant-design/icons": "4.7.0",
|
"@ant-design/icons": "4.7.0",
|
||||||
"@emoji-mart/data": "^1.0.1",
|
"@emoji-mart/data": "^1.0.1",
|
||||||
"@storybook/react": "^6.4.22",
|
"@storybook/react": "^6.4.22",
|
||||||
"antd": "4.18.9",
|
"antd": "^4.20.4",
|
||||||
"autoprefixer": "^10.4.4",
|
"autoprefixer": "^10.4.4",
|
||||||
"chart.js": "3.7.0",
|
"chart.js": "3.7.0",
|
||||||
"chartkick": "4.1.1",
|
"chartkick": "4.1.1",
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"less": "^4.1.2",
|
"less": "^4.1.2",
|
||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0",
|
||||||
"prettier": "2.5.1",
|
"prettier": "2.6.2",
|
||||||
"sass": "^1.50.0",
|
"sass": "^1.50.0",
|
||||||
"sass-loader": "^10.1.1",
|
"sass-loader": "^10.1.1",
|
||||||
"sb": "^6.4.22",
|
"sb": "^6.4.22",
|
||||||
|
|
|
@ -21,13 +21,13 @@ import '../styles/pages.scss';
|
||||||
import '../styles/offline-notice.scss';
|
import '../styles/offline-notice.scss';
|
||||||
|
|
||||||
import { AppProps } from 'next/app';
|
import { AppProps } from 'next/app';
|
||||||
import { useRouter } from 'next/router';
|
import { Router, useRouter } from 'next/router';
|
||||||
|
|
||||||
import AdminLayout from './admin/admin-layout';
|
import AdminLayout from '../components/layouts/admin-layout';
|
||||||
import SimpleLayout from '../components/layouts/SimpleLayout';
|
import SimpleLayout from '../components/layouts/SimpleLayout';
|
||||||
|
|
||||||
function App({ Component, pageProps }: AppProps) {
|
function App({ Component, pageProps }: AppProps) {
|
||||||
const router = useRouter();
|
const router = useRouter() as Router;
|
||||||
if (router.pathname.startsWith('/admin')) {
|
if (router.pathname.startsWith('/admin')) {
|
||||||
return <AdminLayout pageProps={pageProps} Component={Component} router={router} />;
|
return <AdminLayout pageProps={pageProps} Component={Component} router={router} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import TextFieldWithSubmit, {
|
||||||
import { TEXTFIELD_PROPS_FEDERATION_INSTANCE_URL } from '../../utils/config-constants';
|
import { TEXTFIELD_PROPS_FEDERATION_INSTANCE_URL } from '../../utils/config-constants';
|
||||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||||
import { UpdateArgs } from '../../types/config-section';
|
import { UpdateArgs } from '../../types/config-section';
|
||||||
import isValidUrl from '../utils/urls';
|
import isValidUrl from '../../utils/urls';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["config:base"],
|
||||||
"config:base"
|
|
||||||
],
|
|
||||||
"timezone": "America/Los_Angeles",
|
"timezone": "America/Los_Angeles",
|
||||||
"schedule": [
|
"schedule": ["before 8am on Monday"],
|
||||||
"before 8am on Monday"
|
|
||||||
],
|
|
||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"automerge": true
|
"automerge": true
|
||||||
|
@ -15,37 +11,26 @@
|
||||||
},
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": [
|
"matchUpdateTypes": ["minor"],
|
||||||
"minor"
|
|
||||||
],
|
|
||||||
"matchCurrentVersion": "!/^0/",
|
"matchCurrentVersion": "!/^0/",
|
||||||
"automerge": true
|
"automerge": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchDepTypes": [
|
"matchDepTypes": ["devDependencies"],
|
||||||
"devDependencies"
|
|
||||||
],
|
|
||||||
"automerge": true,
|
"automerge": true,
|
||||||
"major": {
|
"major": {
|
||||||
"dependencyDashboardApproval": true
|
"dependencyDashboardApproval": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchPackagePatterns": [
|
"matchPackagePatterns": ["*"],
|
||||||
"*"
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
],
|
|
||||||
"matchUpdateTypes": [
|
|
||||||
"minor",
|
|
||||||
"patch"
|
|
||||||
],
|
|
||||||
"major": {
|
"major": {
|
||||||
"dependencyDashboardApproval": true
|
"dependencyDashboardApproval": true
|
||||||
},
|
},
|
||||||
"groupName": "all non-major dependencies",
|
"groupName": "all non-major dependencies",
|
||||||
"groupSlug": "all-minor-patch",
|
"groupSlug": "all-minor-patch",
|
||||||
"labels": [
|
"labels": ["dependencies"]
|
||||||
"dependencies"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import ServerStatus from '../interfaces/server-status.model';
|
import { ServerStatus } from '../interfaces/server-status.model';
|
||||||
|
|
||||||
const ENDPOINT = `/api/status`;
|
const ENDPOINT = `/api/status`;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { MessageType } from '../interfaces/socket-events';
|
import { MessageType, SocketEvent } from '../interfaces/socket-events';
|
||||||
|
|
||||||
interface SocketMessage {
|
export interface SocketMessage {
|
||||||
type: MessageType;
|
type: MessageType;
|
||||||
data: any;
|
data: any;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export default class WebsocketService {
|
||||||
|
|
||||||
websocketReconnectTimer: ReturnType<typeof setTimeout>;
|
websocketReconnectTimer: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
handleMessage?: (message: SocketMessage) => void;
|
handleMessage?: (message: SocketEvent) => void;
|
||||||
|
|
||||||
constructor(accessToken, path) {
|
constructor(accessToken, path) {
|
||||||
this.accessToken = accessToken;
|
this.accessToken = accessToken;
|
||||||
|
@ -76,7 +76,7 @@ export default class WebsocketService {
|
||||||
// Optimization where multiple events can be sent within a
|
// Optimization where multiple events can be sent within a
|
||||||
// single websocket message. So split them if needed.
|
// single websocket message. So split them if needed.
|
||||||
const messages = e.data.split('\n');
|
const messages = e.data.split('\n');
|
||||||
let message: SocketMessage;
|
let message: SocketEvent;
|
||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
|
|
|
@ -10,9 +10,10 @@ export default {
|
||||||
} as ComponentMeta<typeof ActionButtonRow>;
|
} as ComponentMeta<typeof ActionButtonRow>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const Template: ComponentStory<typeof ActionButtonRow> = args => (
|
const Template: ComponentStory<typeof ActionButtonRow> = args => {
|
||||||
<ActionButtonRow>{args.buttons}</ActionButtonRow>
|
const { buttons } = args as any;
|
||||||
);
|
return <ActionButtonRow>{buttons}</ActionButtonRow>;
|
||||||
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const actions = [
|
const actions = [
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
import BrowserNotifyModal from '../components/modals/BrowserNotifyModal';
|
import BrowserNotifyModal from '../components/modals/BrowserNotifyModal';
|
||||||
|
import AuthModal from '../components/modals/AuthModal';
|
||||||
|
|
||||||
const Example = () => (
|
const Example = () => (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default {
|
||||||
} as ComponentMeta<typeof ChatActionMessage>;
|
} as ComponentMeta<typeof ChatActionMessage>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const Template: ComponentStory<typeof ChatActionMessage> = args => <ChatActionMessage />;
|
const Template: ComponentStory<typeof ChatActionMessage> = args => <ChatActionMessage {...args} />;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const Basic = Template.bind({});
|
export const Basic = Template.bind({});
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default {
|
||||||
} as ComponentMeta<typeof ChatSocialMessage>;
|
} as ComponentMeta<typeof ChatSocialMessage>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const Template: ComponentStory<typeof ChatSocialMessage> = args => <ExamChatSocialMessageple />;
|
const Template: ComponentStory<typeof ChatSocialMessage> = args => <ChatSocialMessage {...args} />;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const Basic = Template.bind({});
|
export const Basic = Template.bind({});
|
||||||
|
|
|
@ -8,8 +8,7 @@ export default {
|
||||||
parameters: {},
|
parameters: {},
|
||||||
} as ComponentMeta<typeof ChatSystemMessage>;
|
} as ComponentMeta<typeof ChatSystemMessage>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const Template: ComponentStory<typeof ChatSystemMessage> = args => <ChatSystemMessage {...args} />;
|
||||||
const Template: ComponentStory<typeof ChatSystemMessage> = args => <ChatSystemMessage />;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const Basic = Template.bind({});
|
export const Basic = Template.bind({});
|
||||||
|
|
|
@ -25,7 +25,6 @@ export function Color(props) {
|
||||||
|
|
||||||
const colorDescriptionStyle = {
|
const colorDescriptionStyle = {
|
||||||
margin: '5px',
|
margin: '5px',
|
||||||
textAlign: 'center',
|
|
||||||
color: 'gray',
|
color: 'gray',
|
||||||
fontSize: '0.8em',
|
fontSize: '0.8em',
|
||||||
};
|
};
|
||||||
|
@ -33,7 +32,9 @@ export function Color(props) {
|
||||||
return (
|
return (
|
||||||
<figure style={containerStyle}>
|
<figure style={containerStyle}>
|
||||||
<div style={colorBlockStyle} />
|
<div style={colorBlockStyle} />
|
||||||
<figcaption style={colorDescriptionStyle}>{color}</figcaption>
|
<figcaption>
|
||||||
|
<span style={colorDescriptionStyle}>{color}</span>
|
||||||
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -44,8 +45,8 @@ Color.propTypes = {
|
||||||
|
|
||||||
const rowStyle = {
|
const rowStyle = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row' as 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap' as 'wrap',
|
||||||
// justifyContent: 'space-around',
|
// justifyContent: 'space-around',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
TreeSelect,
|
TreeSelect,
|
||||||
Switch,
|
Switch,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
|
import { SizeType } from 'antd/lib/config-provider/SizeContext';
|
||||||
|
|
||||||
const FormExample = () => {
|
const FormExample = () => {
|
||||||
const [componentSize, setComponentSize] = useState('default');
|
const [componentSize, setComponentSize] = useState('default');
|
||||||
|
@ -33,7 +34,7 @@ const FormExample = () => {
|
||||||
size: componentSize,
|
size: componentSize,
|
||||||
}}
|
}}
|
||||||
onValuesChange={onFormLayoutChange}
|
onValuesChange={onFormLayoutChange}
|
||||||
size={componentSize}
|
size={componentSize as SizeType}
|
||||||
>
|
>
|
||||||
<Form.Item label="Form Size" name="size">
|
<Form.Item label="Form Size" name="size">
|
||||||
<Radio.Group>
|
<Radio.Group>
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import PropTypes from 'prop-types';
|
export function ImageAsset(props: ImageAssetProps) {
|
||||||
|
|
||||||
export function ImageAsset(props) {
|
|
||||||
const { name, src } = props;
|
const { name, src } = props;
|
||||||
|
|
||||||
const containerStyle = {
|
const containerStyle = {
|
||||||
|
@ -23,7 +21,7 @@ export function ImageAsset(props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorDescriptionStyle = {
|
const colorDescriptionStyle = {
|
||||||
textAlign: 'center',
|
textAlign: 'center' as 'center',
|
||||||
color: 'gray',
|
color: 'gray',
|
||||||
fontSize: '0.8em',
|
fontSize: '0.8em',
|
||||||
};
|
};
|
||||||
|
@ -48,19 +46,20 @@ export function ImageAsset(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Image.propTypes = {
|
interface ImageAssetProps {
|
||||||
name: PropTypes.string.isRequired,
|
name: string;
|
||||||
};
|
src: string;
|
||||||
|
}
|
||||||
|
|
||||||
const rowStyle = {
|
const rowStyle = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row' as 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap' as 'wrap',
|
||||||
// justifyContent: 'space-around',
|
// justifyContent: 'space-around',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ImageRow(props) {
|
export function ImageRow(props: ImageRowProps) {
|
||||||
const { images } = props;
|
const { images } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -72,6 +71,6 @@ export function ImageRow(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageRow.propTypes = {
|
interface ImageRowProps {
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: ImageAssetProps[];
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||||
import {Color, ColorRow} from './Color';
|
import { Color, ColorRow } from './Color';
|
||||||
import {Image, ImageRow} from './ImageAsset';
|
import { Image, ImageRow } from './ImageAsset';
|
||||||
|
|
||||||
import Logo from '../assets/images/logo.svg';
|
import Logo from '../assets/images/logo.svg';
|
||||||
import FediverseColor from '../assets/images/fediverse-color.png';
|
import FediverseColor from '../assets/images/fediverse-color.png';
|
||||||
|
@ -16,73 +16,113 @@ import IsBot from '../assets/images/bot.svg';
|
||||||
|
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
export const images = [{
|
export const images = [
|
||||||
src: Logo,
|
{
|
||||||
name: 'Logo',
|
src: Logo,
|
||||||
}, {
|
name: 'Logo',
|
||||||
src: FediverseColor,
|
},
|
||||||
name: 'Fediverse Color',
|
{
|
||||||
},{
|
src: FediverseColor,
|
||||||
src: FediverseBlack,
|
name: 'Fediverse Color',
|
||||||
name: 'Fediverse Black',
|
},
|
||||||
}, {
|
{
|
||||||
src: Moderator,
|
src: FediverseBlack,
|
||||||
name: 'Moderator',
|
name: 'Fediverse Black',
|
||||||
}, {
|
},
|
||||||
src: IndieAuth,
|
{
|
||||||
name: 'IndieAuth',
|
src: Moderator,
|
||||||
}, {
|
name: 'Moderator',
|
||||||
src: IsBot,
|
},
|
||||||
name: 'Bot Flag',
|
{
|
||||||
}];
|
src: IndieAuth,
|
||||||
|
name: 'IndieAuth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: IsBot,
|
||||||
|
name: 'Bot Flag',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
|
|
||||||
<Story name="Colors">
|
<Story name="Colors"></Story>
|
||||||
</Story>
|
|
||||||
|
|
||||||
<ColorRow colors={['theme-primary-color', 'theme-text-color-secondary']} />
|
<ColorRow colors={['theme-primary-color', 'theme-text-color-secondary']} />
|
||||||
|
|
||||||
## Text
|
## Text
|
||||||
|
|
||||||
<ColorRow colors={['theme-text-color', 'theme-text-color-secondary', 'theme-link-color']} />
|
<ColorRow colors={['theme-text-color', 'theme-text-color-secondary', 'theme-link-color']} />
|
||||||
|
|
||||||
## Backgrounds
|
## Backgrounds
|
||||||
|
|
||||||
<ColorRow colors={['theme-background', 'theme-background-secondary', 'popover-background']} />
|
<ColorRow colors={['theme-background', 'theme-background-secondary', 'popover-background']} />
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
<ColorRow colors={['theme-success-color', 'theme-info-color', 'theme-warning-color', 'theme-error-color']} />
|
|
||||||
|
<ColorRow
|
||||||
|
colors={['theme-success-color', 'theme-info-color', 'theme-warning-color', 'theme-error-color']}
|
||||||
|
/>
|
||||||
|
|
||||||
## Gray
|
## Gray
|
||||||
<ColorRow colors={['color-owncast-gray-100', 'color-owncast-gray-300', 'color-owncast-gray-500', 'color-owncast-gray-700', 'color-owncast-gray-900']} />
|
|
||||||
|
<ColorRow
|
||||||
|
colors={[
|
||||||
|
'color-owncast-gray-100',
|
||||||
|
'color-owncast-gray-300',
|
||||||
|
'color-owncast-gray-500',
|
||||||
|
'color-owncast-gray-700',
|
||||||
|
'color-owncast-gray-900',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
## Purple
|
## Purple
|
||||||
<ColorRow colors={['color-owncast-purple-100', 'color-owncast-purple-300', 'color-owncast-purple-500', 'color-owncast-purple-700', 'color-owncast-purple-900']} />
|
|
||||||
|
<ColorRow
|
||||||
|
colors={[
|
||||||
|
'color-owncast-purple-100',
|
||||||
|
'color-owncast-purple-300',
|
||||||
|
'color-owncast-purple-500',
|
||||||
|
'color-owncast-purple-700',
|
||||||
|
'color-owncast-purple-900',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
## Green
|
## Green
|
||||||
<ColorRow colors={['color-owncast-green-100', 'color-owncast-green-300', 'color-owncast-green-500', 'color-owncast-green-700', 'color-owncast-green-900']} />
|
|
||||||
|
<ColorRow
|
||||||
|
colors={[
|
||||||
|
'color-owncast-green-100',
|
||||||
|
'color-owncast-green-300',
|
||||||
|
'color-owncast-green-500',
|
||||||
|
'color-owncast-green-700',
|
||||||
|
'color-owncast-green-900',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
## Orange
|
## Orange
|
||||||
<ColorRow colors={['color-owncast-orange-100', 'color-owncast-orange-300', 'color-owncast-orange-500', 'color-owncast-orange-700', 'color-owncast-orange-900']} />
|
|
||||||
|
<ColorRow
|
||||||
|
colors={[
|
||||||
|
'color-owncast-orange-100',
|
||||||
|
'color-owncast-orange-300',
|
||||||
|
'color-owncast-orange-500',
|
||||||
|
'color-owncast-orange-700',
|
||||||
|
'color-owncast-orange-900',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
# Font
|
# Font
|
||||||
|
|
||||||
[Inter font](https://rsms.me/inter/)
|
[Inter font](https://rsms.me/inter/)
|
||||||
|
|
||||||
<Story name="Fonts">
|
<Story name="Fonts">
|
||||||
<Canvas style={{color: 'var(--theme-text-color-secondary)'}}>
|
<Canvas style={{ color: 'var(--theme-text-color-secondary)' }}>
|
||||||
{getComputedStyle(document.documentElement).getPropertyValue('--theme-font-family')}
|
{getComputedStyle(document.documentElement).getPropertyValue('--theme-font-family')}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</Story>
|
</Story>
|
||||||
|
|
||||||
# Images
|
# Images
|
||||||
|
|
||||||
<Story name="Images and Icons">
|
<Story name="Images and Icons"></Story>
|
||||||
</Story>
|
|
||||||
|
|
||||||
<ImageRow images={images} />
|
<ImageRow images={images} />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Tabs, Radio } from 'antd';
|
|
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
|
||||||
|
|
||||||
class TabsExample extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = { size: 'small' };
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = e => {
|
|
||||||
this.setState({ size: e.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { size } = this.state;
|
|
||||||
const { type } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Radio.Group value={size} onChange={this.onChange} style={{ marginBottom: 16 }}>
|
|
||||||
<Radio.Button value="small">Small</Radio.Button>
|
|
||||||
<Radio.Button value="default">Default</Radio.Button>
|
|
||||||
<Radio.Button value="large">Large</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
|
|
||||||
<Tabs defaultActiveKey="1" type={type} size={size}>
|
|
||||||
<TabPane tab="Card Tab 1" key="1">
|
|
||||||
Content of card tab 1
|
|
||||||
</TabPane>
|
|
||||||
<TabPane tab="Card Tab 2" key="2">
|
|
||||||
Content of card tab 2
|
|
||||||
</TabPane>
|
|
||||||
<TabPane tab="Card Tab 3" key="3">
|
|
||||||
Content of card tab 3
|
|
||||||
</TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'example/Tabs',
|
|
||||||
component: Tabs,
|
|
||||||
} as ComponentMeta<typeof Tabs>;
|
|
||||||
|
|
||||||
const Template: ComponentStory<typeof Tabs> = args => <TabsExample {...args} />;
|
|
||||||
|
|
||||||
export const Card = Template.bind({});
|
|
||||||
Card.args = { type: 'card' };
|
|
||||||
|
|
||||||
export const Basic = Template.bind({});
|
|
||||||
Basic.args = { type: '' };
|
|
||||||
|
|
||||||
TabsExample.propTypes = {
|
|
||||||
type: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
TabsExample.defaultProps = {
|
|
||||||
type: '',
|
|
||||||
};
|
|
|
@ -1,10 +1,11 @@
|
||||||
# Style Definitions
|
# Style Definitions
|
||||||
|
|
||||||
Read more about [Style Dictionary](https://amzn.github.io/style-dictionary)
|
Read more about [Style Dictionary](https://amzn.github.io/style-dictionary)
|
||||||
|
|
||||||
## Add
|
## Add
|
||||||
|
|
||||||
Add to the `tokens/**/*.yaml` files to add or modify style values.
|
Add to the `tokens/**/*.yaml` files to add or modify style values.
|
||||||
|
|
||||||
## Generating
|
## Generating
|
||||||
|
|
||||||
Run `npm run build-styles` to regenerate the CSS variables.
|
Run `npm run build-styles` to regenerate the CSS variables.
|
||||||
|
|
|
@ -2,33 +2,33 @@
|
||||||
# You can find the variable names to override at:
|
# You can find the variable names to override at:
|
||||||
# https://github.com/ant-design/ant-design/blob/master/components/style/themes/dark.less
|
# https://github.com/ant-design/ant-design/blob/master/components/style/themes/dark.less
|
||||||
|
|
||||||
text-color:
|
text-color:
|
||||||
value: "var(--theme-text-color)"
|
value: 'var(--theme-text-color)'
|
||||||
text-color-secondry:
|
text-color-secondry:
|
||||||
value: "var(--theme-text-color-secondary)"
|
value: 'var(--theme-text-color-secondary)'
|
||||||
link-color:
|
link-color:
|
||||||
value: "var(--theme-link-color)"
|
value: 'var(--theme-link-color)'
|
||||||
popover-background:
|
popover-background:
|
||||||
value: "var(--theme-background)"
|
value: 'var(--theme-background)'
|
||||||
background-color-light:
|
background-color-light:
|
||||||
value: "var(--theme-background-secondary)"
|
value: 'var(--theme-background-secondary)'
|
||||||
|
|
||||||
# These values require explicit colors and cannot take css variables.
|
# These values require explicit colors and cannot take css variables.
|
||||||
primary-color:
|
primary-color:
|
||||||
value: "{color.owncast.purple.500.value}"
|
value: '{color.owncast.purple.500.value}'
|
||||||
info-color:
|
info-color:
|
||||||
value: "{color.owncast.gray.500.value}"
|
value: '{color.owncast.gray.500.value}'
|
||||||
success-color:
|
success-color:
|
||||||
value: "{color.owncast.green.500.value}"
|
value: '{color.owncast.green.500.value}'
|
||||||
warning-color:
|
warning-color:
|
||||||
value: "{color.owncast.orange.500.value}"
|
value: '{color.owncast.orange.500.value}'
|
||||||
error-color:
|
error-color:
|
||||||
value: "{color.owncast.red.500.value}"
|
value: '{color.owncast.red.500.value}'
|
||||||
purple-base:
|
purple-base:
|
||||||
value: "{color.owncast.purple.500.value}"
|
value: '{color.owncast.purple.500.value}'
|
||||||
green-base:
|
green-base:
|
||||||
value: "{color.owncast.green.500.value}"
|
value: '{color.owncast.green.500.value}'
|
||||||
red-base:
|
red-base:
|
||||||
value: "{color.owncast.red.500.value}"
|
value: '{color.owncast.red.500.value}'
|
||||||
orange-base:
|
orange-base:
|
||||||
value: "{color.owncast.orange.500.value}"
|
value: '{color.owncast.orange.500.value}'
|
||||||
|
|
|
@ -5,32 +5,32 @@
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
primary-color:
|
primary-color:
|
||||||
value: "{color.owncast.purple.500.value}"
|
value: '{color.owncast.purple.500.value}'
|
||||||
comment: "The primary color of the application used for rendering controls."
|
comment: 'The primary color of the application used for rendering controls.'
|
||||||
text-color:
|
text-color:
|
||||||
value: "{color.owncast.gray.300.value}"
|
value: '{color.owncast.gray.300.value}'
|
||||||
comment: "The color of the text in the application."
|
comment: 'The color of the text in the application.'
|
||||||
text-color-secondary:
|
text-color-secondary:
|
||||||
value: "{color.owncast.gray.500.value}"
|
value: '{color.owncast.gray.500.value}'
|
||||||
link-color:
|
link-color:
|
||||||
value: "{color.owncast.purple.500.value}"
|
value: '{color.owncast.purple.500.value}'
|
||||||
font-family:
|
font-family:
|
||||||
value: "{font.owncast.family.value}"
|
value: '{font.owncast.family.value}'
|
||||||
background:
|
background:
|
||||||
value: "{color.owncast.background.value}"
|
value: '{color.owncast.background.value}'
|
||||||
comment: "The main background color of the page."
|
comment: 'The main background color of the page.'
|
||||||
background-secondary:
|
background-secondary:
|
||||||
value: "{color.owncast.background-secondary.value}"
|
value: '{color.owncast.background-secondary.value}'
|
||||||
comment: "A secondary background color used in sections and controls."
|
comment: 'A secondary background color used in sections and controls.'
|
||||||
rounded-corners:
|
rounded-corners:
|
||||||
value: "5px"
|
value: '5px'
|
||||||
comment: "The radius of rounded corners used in places."
|
comment: 'The radius of rounded corners used in places.'
|
||||||
|
|
||||||
success-color:
|
success-color:
|
||||||
value: "{color.owncast.green.500.value}"
|
value: '{color.owncast.green.500.value}'
|
||||||
info-color:
|
info-color:
|
||||||
value: "{color.owncast.purple.300.value}"
|
value: '{color.owncast.purple.300.value}'
|
||||||
warning-color:
|
warning-color:
|
||||||
value: "{color.owncast.orange.500.value}"
|
value: '{color.owncast.orange.500.value}'
|
||||||
error-color:
|
error-color:
|
||||||
value: "{color.owncast.red.500.value}"
|
value: '{color.owncast.red.500.value}'
|
||||||
|
|
|
@ -4,79 +4,79 @@ color:
|
||||||
owncast:
|
owncast:
|
||||||
purple:
|
purple:
|
||||||
100:
|
100:
|
||||||
value: "#f4ebff"
|
value: '#f4ebff'
|
||||||
300:
|
300:
|
||||||
value: "#d6bbfb"
|
value: '#d6bbfb'
|
||||||
500:
|
500:
|
||||||
value: "#9e77ed"
|
value: '#9e77ed'
|
||||||
700:
|
700:
|
||||||
value: "#6941c6"
|
value: '#6941c6'
|
||||||
900:
|
900:
|
||||||
value: "#42307d"
|
value: '#42307d'
|
||||||
|
|
||||||
green:
|
green:
|
||||||
100:
|
100:
|
||||||
value: "#d15ad5"
|
value: '#d15ad5'
|
||||||
300:
|
300:
|
||||||
value: "#6ce9a6"
|
value: '#6ce9a6'
|
||||||
500:
|
500:
|
||||||
value: "#12b76a"
|
value: '#12b76a'
|
||||||
700:
|
700:
|
||||||
value: "#027a48"
|
value: '#027a48'
|
||||||
900:
|
900:
|
||||||
value: "#054f31"
|
value: '#054f31'
|
||||||
|
|
||||||
red:
|
red:
|
||||||
100:
|
100:
|
||||||
value: "#fee4e2"
|
value: '#fee4e2'
|
||||||
300:
|
300:
|
||||||
value: "#fda29b"
|
value: '#fda29b'
|
||||||
500:
|
500:
|
||||||
value: "#f04438"
|
value: '#f04438'
|
||||||
700:
|
700:
|
||||||
value: "#b42318"
|
value: '#b42318'
|
||||||
900:
|
900:
|
||||||
value: "#7a271a"
|
value: '#7a271a'
|
||||||
|
|
||||||
orange:
|
orange:
|
||||||
100:
|
100:
|
||||||
value: "#fef0c7"
|
value: '#fef0c7'
|
||||||
300:
|
300:
|
||||||
value: "#fec84b"
|
value: '#fec84b'
|
||||||
500:
|
500:
|
||||||
value: "#f79009"
|
value: '#f79009'
|
||||||
700:
|
700:
|
||||||
value: "#b54708"
|
value: '#b54708'
|
||||||
900:
|
900:
|
||||||
value: "#93370d"
|
value: '#93370d'
|
||||||
|
|
||||||
gray:
|
gray:
|
||||||
100:
|
100:
|
||||||
value: "#f2f4f7"
|
value: '#f2f4f7'
|
||||||
300:
|
300:
|
||||||
value: "#d0d5dd"
|
value: '#d0d5dd'
|
||||||
500:
|
500:
|
||||||
value: "#667085"
|
value: '#667085'
|
||||||
700:
|
700:
|
||||||
value: "#344054"
|
value: '#344054'
|
||||||
900:
|
900:
|
||||||
value: "#101828"
|
value: '#101828'
|
||||||
|
|
||||||
logo:
|
logo:
|
||||||
purple:
|
purple:
|
||||||
value: "rgba(120, 113, 255, 1)"
|
value: 'rgba(120, 113, 255, 1)'
|
||||||
pink:
|
pink:
|
||||||
value: "rgba(201, 139, 254, 1)"
|
value: 'rgba(201, 139, 254, 1)'
|
||||||
blue:
|
blue:
|
||||||
value: "rgba(32, 134, 225, 1)"
|
value: 'rgba(32, 134, 225, 1)'
|
||||||
|
|
||||||
background:
|
background:
|
||||||
value: "rgba(27, 26, 38, 1)"
|
value: 'rgba(27, 26, 38, 1)'
|
||||||
background-secondary:
|
background-secondary:
|
||||||
value: "rgba(22, 21, 31, 1)"
|
value: 'rgba(22, 21, 31, 1)'
|
||||||
|
|
||||||
font:
|
font:
|
||||||
owncast:
|
owncast:
|
||||||
family:
|
family:
|
||||||
value: "'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
value: "'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||||
|
@ -85,28 +85,28 @@ font:
|
||||||
# Values used in the admin and should be migrated to variables or removed.
|
# Values used in the admin and should be migrated to variables or removed.
|
||||||
# See ant-overrides.scss.
|
# See ant-overrides.scss.
|
||||||
owncast-purple:
|
owncast-purple:
|
||||||
value: "{color.owncast.logo.purple.value}"
|
value: '{color.owncast.logo.purple.value}'
|
||||||
owncast-purple-25:
|
owncast-purple-25:
|
||||||
value: "rgba(120, 113, 255, 0.25)"
|
value: 'rgba(120, 113, 255, 0.25)'
|
||||||
owncast-purple-50:
|
owncast-purple-50:
|
||||||
value: "rgba(120, 113, 255, 0.5)"
|
value: 'rgba(120, 113, 255, 0.5)'
|
||||||
online-color:
|
online-color:
|
||||||
value: "#73dd3f"
|
value: '#73dd3f'
|
||||||
offline-color:
|
offline-color:
|
||||||
value: "#999"
|
value: '#999'
|
||||||
pink:
|
pink:
|
||||||
value: "{color.owncast.logo.pink.value}"
|
value: '{color.owncast.logo.pink.value}'
|
||||||
purple:
|
purple:
|
||||||
value: "{color.owncast.purple.500.value}"
|
value: '{color.owncast.purple.500.value}'
|
||||||
blue:
|
blue:
|
||||||
value: "{color.owncast.logo.blue.value}"
|
value: '{color.owncast.logo.blue.value}'
|
||||||
white-88:
|
white-88:
|
||||||
value: "{color.owncast.gray.500.value}"
|
value: '{color.owncast.gray.500.value}'
|
||||||
purple-dark:
|
purple-dark:
|
||||||
value: "{color.owncast.purple.900.value}"
|
value: '{color.owncast.purple.900.value}'
|
||||||
default-link-color:
|
default-link-color:
|
||||||
value: "{color.owncast.purple.700.value}"
|
value: '{color.owncast.purple.700.value}'
|
||||||
default-bg-color:
|
default-bg-color:
|
||||||
value: "{color.owncast.background.value}"
|
value: '{color.owncast.background.value}'
|
||||||
default-text-color:
|
default-text-color:
|
||||||
value: "{color.owncast.gray.100.value}"
|
value: '{color.owncast.gray.100.value}'
|
||||||
|
|
|
@ -356,7 +356,6 @@ textarea.ant-input {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// SELECT
|
// SELECT
|
||||||
.ant-select-dropdown {
|
.ant-select-dropdown {
|
||||||
background-color: var(--black);
|
background-color: var(--black);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// styles for Storage config section
|
// styles for Storage config section
|
||||||
|
|
||||||
|
|
||||||
.edit-storage-container {
|
.edit-storage-container {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
@ -21,9 +20,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.edit-server-details-container {
|
.edit-server-details-container {
|
||||||
|
|
||||||
// Do something special for the stream key field
|
// Do something special for the stream key field
|
||||||
.field-streamkey-container {
|
.field-streamkey-container {
|
||||||
margin-bottom: 1.5em;
|
margin-bottom: 1.5em;
|
||||||
|
@ -33,7 +30,7 @@
|
||||||
.left-side {
|
.left-side {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textfield-with-submit-container {
|
.textfield-with-submit-container {
|
||||||
|
@ -43,7 +40,7 @@
|
||||||
.streamkey-actions {
|
.streamkey-actions {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
button {
|
button {
|
||||||
margin: .25em;
|
margin: 0.25em;
|
||||||
}
|
}
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
|
@ -54,5 +51,4 @@
|
||||||
.advanced-settings {
|
.advanced-settings {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -19,40 +19,37 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: .75rem;
|
font-size: 0.75rem;
|
||||||
.status-icon {
|
.status-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: .5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* TIP CONTAINER BASE */
|
/* TIP CONTAINER BASE */
|
||||||
.field-tip {
|
.field-tip {
|
||||||
font-size: .8em;
|
font-size: 0.8em;
|
||||||
color: var(--white-50);
|
color: var(--white-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Ideal for wrapping each Textfield on a page with many text fields in a row. This div will alternate colors and look like a table.
|
Ideal for wrapping each Textfield on a page with many text fields in a row. This div will alternate colors and look like a table.
|
||||||
*/
|
*/
|
||||||
.field-container {
|
.field-container {
|
||||||
padding: .85em 0 .5em;
|
padding: 0.85em 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* SEGMENT SLIDER GROUP WITH SELECTED NOTE, OR STATUS */
|
/* SEGMENT SLIDER GROUP WITH SELECTED NOTE, OR STATUS */
|
||||||
.segment-slider-container {
|
.segment-slider-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 1em 2em .75em;
|
padding: 1em 2em 0.75em;
|
||||||
background-color: var(--owncast-purple-25);
|
background-color: var(--owncast-purple-25);
|
||||||
border-radius: var(--container-border-radius);
|
border-radius: var(--container-border-radius);
|
||||||
|
|
||||||
.status-container {
|
.status-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: .5em auto;
|
margin: 0.5em auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +57,7 @@ Ideal for wrapping each Textfield on a page with many text fields in a row. This
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 3em auto 0;
|
margin: 3em auto 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: .75em;
|
font-size: 0.75em;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
@ -69,7 +66,6 @@ Ideal for wrapping each Textfield on a page with many text fields in a row. This
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.segment-tip {
|
.segment-tip {
|
||||||
width: 10em;
|
width: 10em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -81,4 +77,4 @@ Ideal for wrapping each Textfield on a page with many text fields in a row. This
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ Base styles for
|
||||||
padding-right: 1.25em;
|
padding-right: 1.25em;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: var(--form-label-container-width);
|
width: var(--form-label-container-width);
|
||||||
margin: .2em 0;
|
margin: 0.2em 0;
|
||||||
}
|
}
|
||||||
.formfield-label {
|
.formfield-label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -35,7 +35,7 @@ Base styles for
|
||||||
&::before {
|
&::before {
|
||||||
content: '*';
|
content: '*';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: .25em;
|
margin-right: 0.25em;
|
||||||
color: var(--ant-error);
|
color: var(--ant-error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ Base styles for
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-container {
|
.status-container {
|
||||||
margin: .25em;
|
margin: 0.25em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
&.empty {
|
&.empty {
|
||||||
|
@ -62,9 +62,9 @@ Base styles for
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-tip {
|
.field-tip {
|
||||||
margin: .5em .5em;
|
margin: 0.5em 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
|
@ -103,7 +103,7 @@ Base styles for
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
.label-spacer {
|
.label-spacer {
|
||||||
width: var(--form-label-container-width);
|
width: var(--form-label-container-width);
|
||||||
}
|
}
|
||||||
.lower-content {
|
.lower-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -117,7 +117,7 @@ Base styles for
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.status-container {
|
.status-container {
|
||||||
margin: .5em;
|
margin: 0.5em;
|
||||||
&.empty {
|
&.empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ Base styles for
|
||||||
}
|
}
|
||||||
.update-button-container {
|
.update-button-container {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
margin: .25em 0;
|
margin: 0.25em 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +137,6 @@ Base styles for
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.label-spacer {
|
.label-spacer {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -145,12 +144,11 @@ Base styles for
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* TOGGLE SWITCH CONTAINER BASE */
|
/* TOGGLE SWITCH CONTAINER BASE */
|
||||||
.toggleswitch-container {
|
.toggleswitch-container {
|
||||||
margin: 2em 0 1em;
|
margin: 2em 0 1em;
|
||||||
|
|
||||||
.label-side {
|
.label-side {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +157,7 @@ Base styles for
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.status-container {
|
.status-container {
|
||||||
width: auto;
|
width: auto;
|
||||||
margin: 0 0 0 1em;
|
margin: 0 0 0 1em;
|
||||||
|
|
|
@ -5,9 +5,8 @@
|
||||||
// --header-h: 64px;
|
// --header-h: 64px;
|
||||||
--chat-w: 300px;
|
--chat-w: 300px;
|
||||||
--chat-input-h: 40.5px;
|
--chat-input-h: 40.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// misc styling for various /pages
|
// misc styling for various /pages
|
||||||
|
|
||||||
|
|
||||||
// .help-page {
|
// .help-page {
|
||||||
// .ant-result-image {
|
// .ant-result-image {
|
||||||
// height: 100px;
|
// height: 100px;
|
||||||
|
@ -11,9 +10,9 @@
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
.upgrade-page {
|
.upgrade-page {
|
||||||
h2,h3 {
|
h2,
|
||||||
|
h3 {
|
||||||
color: var(--pink);
|
color: var(--pink);
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
// Do not edit directly
|
// Do not edit directly
|
||||||
// Generated on Sat, 07 May 2022 17:24:18 GMT
|
// Generated on Sat, 07 May 2022 17:24:18 GMT
|
||||||
|
|
||||||
|
@ -20,7 +19,9 @@
|
||||||
@theme-text-color: #d0d5dd; // The color of the text in the application.
|
@theme-text-color: #d0d5dd; // The color of the text in the application.
|
||||||
@theme-text-color-secondary: #667085;
|
@theme-text-color-secondary: #667085;
|
||||||
@theme-link-color: #9e77ed;
|
@theme-link-color: #9e77ed;
|
||||||
@theme-font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
@theme-font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||||
|
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
@theme-background: #1b1a26; // The main background color of the page.
|
@theme-background: #1b1a26; // The main background color of the page.
|
||||||
@theme-background-secondary: #16151f; // A secondary background color used in sections and controls.
|
@theme-background-secondary: #16151f; // A secondary background color used in sections and controls.
|
||||||
@theme-rounded-corners: 5px; // The radius of rounded corners.
|
@theme-rounded-corners: 5px; // The radius of rounded corners.
|
||||||
|
@ -58,7 +59,9 @@
|
||||||
@color-owncast-logo-blue: #2086e1;
|
@color-owncast-logo-blue: #2086e1;
|
||||||
@color-owncast-background: #1b1a26;
|
@color-owncast-background: #1b1a26;
|
||||||
@color-owncast-background-secondary: #16151f;
|
@color-owncast-background-secondary: #16151f;
|
||||||
@font-owncast-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
@font-owncast-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||||
|
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
@owncast-purple: #7871ff;
|
@owncast-purple: #7871ff;
|
||||||
@owncast-purple-25: rgba(120, 113, 255, 0.25);
|
@owncast-purple-25: rgba(120, 113, 255, 0.25);
|
||||||
@owncast-purple-50: rgba(120, 113, 255, 0.5);
|
@owncast-purple-50: rgba(120, 113, 255, 0.5);
|
||||||
|
@ -71,4 +74,4 @@
|
||||||
@purple-dark: #42307d;
|
@purple-dark: #42307d;
|
||||||
@default-link-color: #6941c6;
|
@default-link-color: #6941c6;
|
||||||
@default-bg-color: #1b1a26;
|
@default-bg-color: #1b1a26;
|
||||||
@default-text-color: #f2f4f7;
|
@default-text-color: #f2f4f7;
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
|
@ -19,12 +15,6 @@
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"next-env.d.ts",
|
"exclude": ["node_modules"]
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,15 +17,15 @@ export default function useWindowSize() {
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listener
|
// Add event listener
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
// Call handler right away so state gets updated with initial window size
|
// Call handler right away so state gets updated with initial window size
|
||||||
handleResize();
|
handleResize();
|
||||||
|
|
||||||
// Remove event listener on cleanup
|
// Remove event listener on cleanup
|
||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
}, []); // Empty array ensures that effect is only run on mount
|
}, []); // Empty array ensures that effect is only run on mount
|
||||||
|
|
||||||
return windowSize;
|
return windowSize;
|
||||||
|
|
|
@ -5,11 +5,15 @@ export default function isValidUrl(url: string): boolean {
|
||||||
const validProtocols = ['http:', 'https:'];
|
const validProtocols = ['http:', 'https:'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const validationObject = new URL(url);
|
const validationObject = new URL(url);
|
||||||
if (validationObject.protocol === '' || validationObject.hostname === '' || !validProtocols.includes(validationObject.protocol)) {
|
if (
|
||||||
return false;
|
validationObject.protocol === '' ||
|
||||||
}
|
validationObject.hostname === '' ||
|
||||||
} catch(e) {
|
!validProtocols.includes(validationObject.protocol)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue