mirror of
https://github.com/owncast/owncast.git
synced 2024-11-23 21:28:29 +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
|
||||
with:
|
||||
# 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
|
||||
only_changed: true
|
||||
env:
|
||||
|
|
|
@ -40,6 +40,7 @@ module.exports = {
|
|||
'@typescript-eslint/no-use-before-define': [1],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': ['error'],
|
||||
'no-restricted-exports': 'off',
|
||||
'react/jsx-no-target-blank': [
|
||||
1,
|
||||
{
|
||||
|
|
22
web/.vscode/settings.json
vendored
22
web/.vscode/settings.json
vendored
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"Owncast",
|
||||
"antd",
|
||||
"bitrates",
|
||||
"chartkick",
|
||||
"framerates",
|
||||
"kbps",
|
||||
"linkify",
|
||||
"paypal",
|
||||
"toggleswitch"
|
||||
]
|
||||
"cSpell.words": [
|
||||
"Owncast",
|
||||
"antd",
|
||||
"bitrates",
|
||||
"chartkick",
|
||||
"framerates",
|
||||
"kbps",
|
||||
"linkify",
|
||||
"paypal",
|
||||
"toggleswitch"
|
||||
]
|
||||
}
|
|
@ -8,7 +8,7 @@ The Owncast web frontend is a [Next.js](https://nextjs.org/) project with [React
|
|||
|
||||
**First**, install the dependencies.
|
||||
|
||||
```npm install --include=dev```
|
||||
`npm install --include=dev`
|
||||
|
||||
### Run the web project
|
||||
|
||||
|
@ -16,13 +16,13 @@ Make sure you're running an instance of Owncast on localhost:8080, as your copy
|
|||
|
||||
**Next**, start the web project with npm.
|
||||
|
||||
```npm run dev```
|
||||
`npm run dev`
|
||||
|
||||
### Components and Styles
|
||||
|
||||
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
|
||||
isolation.
|
||||
|
@ -63,4 +63,4 @@ We are currently experimenting with using [Storybook](https://storybook.js.org/)
|
|||
|
||||
To work with Storybook:
|
||||
|
||||
```npm run storybook```
|
||||
`npm run storybook`
|
||||
|
|
|
@ -4,5 +4,6 @@ interface Props {
|
|||
|
||||
export default function CustomPageContent(props: Props) {
|
||||
const { content } = props;
|
||||
// eslint-disable-next-line react/no-danger
|
||||
return <div dangerouslySetInnerHTML={{ __html: content }} />;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
interface Props {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default function PageLogo(props: Props) {
|
||||
export default function PageLogo() {
|
||||
return <div>Pimary logo component goes here</div>;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { SocialLink } from '../interfaces/social-link.model';
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
links: SocialLink[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function SocialLinks(props: Props) {
|
||||
return <div>Social links component goes here</div>;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Button } from 'antd';
|
||||
import { useState } from 'react';
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
message: ChatMessage;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function ChatSystemMessage(props: Props) {
|
||||
return <div>Component goes here</div>;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { Spin } from 'antd';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||
import { ChatState } from '../../interfaces/application-state';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
interface 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';
|
||||
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||
|
||||
interface Props {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function ChatTextField(props: Props) {
|
||||
const [value, setValue] = useState('');
|
||||
const [showEmojis, setShowEmojis] = useState(false);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.root {
|
||||
height: 2rem;
|
||||
color: var(--black);
|
||||
height: 2rem;
|
||||
color: var(--black);
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
import { SmileOutlined } from '@ant-design/icons';
|
||||
import { Button, Popover } from 'antd';
|
||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
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 EmojiPicker from './EmojiPicker';
|
||||
import WebsocketService from '../../../services/websocket-service';
|
||||
import { websocketServiceAtom } from '../../stores/ClientConfigStore';
|
||||
import { MessageType } from '../../../interfaces/socket-events';
|
||||
import s from './ChatTextField.module.scss';
|
||||
|
||||
type CustomElement = { type: 'paragraph'; children: CustomText[] };
|
||||
type CustomText = { text: string };
|
||||
|
@ -25,24 +24,30 @@ interface Props {
|
|||
value?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const Image = ({ element }) => (
|
||||
<img
|
||||
// eslint-disable-next-line no-undef
|
||||
// eslint-disable-next-line react/prop-types
|
||||
src={element.url}
|
||||
alt="emoji"
|
||||
style={{ display: 'inline', position: 'relative', width: '30px', bottom: '10px' }}
|
||||
/>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const insertImage = (editor, url) => {
|
||||
const text = { text: '' };
|
||||
const image: ImageElement = { type: 'image', url, children: [text] };
|
||||
Transforms.insertNodes(editor, image);
|
||||
// const text = { text: '' };
|
||||
// const image: ImageElement = { type: 'image', url, children: [text] };
|
||||
// Transforms.insertNodes(editor, image);
|
||||
};
|
||||
|
||||
const withImages = editor => {
|
||||
const { isVoid } = editor;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
editor.isVoid = element => (element.type === 'image' ? true : isVoid(element));
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
editor.isInline = element => element.type === 'image';
|
||||
|
||||
return editor;
|
||||
|
@ -52,13 +57,13 @@ export type EmptyText = {
|
|||
text: string;
|
||||
};
|
||||
|
||||
type ImageElement = {
|
||||
type: 'image';
|
||||
url: string;
|
||||
children: EmptyText[];
|
||||
};
|
||||
// type ImageElement = {
|
||||
// type: 'image';
|
||||
// url: string;
|
||||
// children: EmptyText[];
|
||||
// };
|
||||
|
||||
const Element = props => {
|
||||
const Element = (props: any) => {
|
||||
const { attributes, children, element } = props;
|
||||
|
||||
switch (element.type) {
|
||||
|
@ -71,10 +76,10 @@ const Element = props => {
|
|||
|
||||
const serialize = node => {
|
||||
if (Text.isText(node)) {
|
||||
let string = node.text;
|
||||
if (node.bold) {
|
||||
string = `<strong>${string}</strong>`;
|
||||
}
|
||||
const string = node.text;
|
||||
// if (node.bold) {
|
||||
// string = `<strong>${string}</strong>`;
|
||||
// }
|
||||
return string;
|
||||
}
|
||||
|
||||
|
@ -90,8 +95,9 @@ const serialize = node => {
|
|||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function ChatTextField(props: Props) {
|
||||
const { value: originalValue } = props;
|
||||
// const { value: originalValue } = props;
|
||||
const [showEmojis, setShowEmojis] = useState(false);
|
||||
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||
const [editor] = useState(() => withImages(withReact(createEditor())));
|
||||
|
@ -113,7 +119,7 @@ export default function ChatTextField(props: Props) {
|
|||
Transforms.delete(editor);
|
||||
};
|
||||
|
||||
const handleChange = e => {};
|
||||
const handleChange = () => {};
|
||||
|
||||
const handleEmojiSelect = emoji => {
|
||||
console.log(emoji);
|
||||
|
@ -135,19 +141,12 @@ export default function ChatTextField(props: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
const initialValue = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: originalValue }],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Slate editor={editor} value={initialValue} onChange={handleChange}>
|
||||
<Slate editor={editor} value={[]} onChange={handleChange}>
|
||||
<Editable
|
||||
onKeyDown={onKeyDown}
|
||||
renderElement={props => <Element {...props} />}
|
||||
renderElement={p => <Element {...p} />}
|
||||
placeholder="Chat message goes here..."
|
||||
/>
|
||||
</Slate>
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
import data from '@emoji-mart/data';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
// import data from '@emoji-mart/data';
|
||||
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();
|
||||
|
||||
// TODO: Pull this custom emoji data in from the emoji API.
|
||||
const custom = [
|
||||
{
|
||||
emojis: [
|
||||
{
|
||||
id: 'party_parrot',
|
||||
name: 'Party Parrot',
|
||||
keywords: ['dance', 'dancing'],
|
||||
skins: [{ src: 'https://watch.owncast.online/img/emoji/bluntparrot.gif' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
// const custom = [
|
||||
// {
|
||||
// emojis: [
|
||||
// {
|
||||
// id: 'party_parrot',
|
||||
// name: 'Party Parrot',
|
||||
// keywords: ['dance', 'dancing'],
|
||||
// skins: [{ src: 'https://watch.owncast.online/img/emoji/bluntparrot.gif' }],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ];
|
||||
|
||||
// TODO: Fix the emoji picker from throwing errors.
|
||||
// useEffect(() => {
|
||||
|
|
|
@ -7,7 +7,9 @@ interface Props {
|
|||
|
||||
export default function ChatUserMessage(props: Props) {
|
||||
const { message, showModeratorMenu } = props;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { body, user, timestamp } = message;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { displayName, displayColor } = user;
|
||||
|
||||
// TODO: Convert displayColor (a hue) to a usable color.
|
||||
|
|
|
@ -101,45 +101,6 @@ export function convertToText(str = '') {
|
|||
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) {
|
||||
const emojiUrl = isCustom ? data.emoji : data.url;
|
||||
const emojiName = (
|
||||
|
@ -156,6 +117,7 @@ export function trimNbsp(html) {
|
|||
export function emojify(HTML, emojiList) {
|
||||
const textValue = convertToText(HTML);
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let lastPos = textValue.length; lastPos >= 0; lastPos--) {
|
||||
const endPos = textValue.lastIndexOf(':', lastPos);
|
||||
if (endPos <= 0) {
|
||||
|
@ -170,8 +132,9 @@ export function emojify(HTML, emojiList) {
|
|||
emojiItem => emojiItem.name.toLowerCase() === typedEmoji.toLowerCase(),
|
||||
);
|
||||
|
||||
if (emojiIndex != -1) {
|
||||
if (emojiIndex !== -1) {
|
||||
const emojiImgElement = createEmojiMarkup(emojiList[emojiIndex], true);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
HTML = HTML.replace(`:${typedEmoji}:`, emojiImgElement);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import s from './Logo.module.scss'
|
||||
import s from './Logo.module.scss';
|
||||
|
||||
interface Props {
|
||||
variant: 'simple' | 'contrast'
|
||||
variant: 'simple' | 'contrast';
|
||||
}
|
||||
|
||||
export default function Logo({variant = 'simple'}: Props) {
|
||||
const rootClassName = cn(
|
||||
s.root,
|
||||
{
|
||||
[s.simple]: variant === 'simple',
|
||||
[s.contrast]: variant === 'contrast',
|
||||
}
|
||||
)
|
||||
export default function Logo({ variant = 'simple' }: Props) {
|
||||
const rootClassName = cn(s.root, {
|
||||
[s.simple]: variant === 'simple',
|
||||
[s.contrast]: variant === 'contrast',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
|
|
|
@ -6,8 +6,8 @@ import { ChatState, ChatVisibilityState } from '../../../interfaces/application-
|
|||
import s from './UserDropdown.module.scss';
|
||||
|
||||
interface Props {
|
||||
username?: string;
|
||||
chatState?: ChatState;
|
||||
username: string;
|
||||
chatState: ChatState;
|
||||
}
|
||||
|
||||
export default function UserDropdown({ username = 'test-user', chatState }: Props) {
|
||||
|
@ -44,11 +44,6 @@ export default function UserDropdown({ username = 'test-user', chatState }: Prop
|
|||
<DownOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
{/*
|
||||
<button type="button" className="ant-dropdown-link" onClick={e => e.preventDefault()}>
|
||||
{username} <DownOutlined />
|
||||
</button>
|
||||
*/}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1 +1 @@
|
|||
export { default } from './UserDropdown'
|
||||
export { default } from './UserDropdown';
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export { default as UserDropdown } from './UserDropdown'
|
||||
export { default as OwncastLogo } from './Logo'
|
||||
export { default as UserDropdown } from './UserDropdown';
|
||||
export { default as OwncastLogo } from './Logo';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AppProps } from 'next/app';
|
||||
import ServerStatusProvider from '../../utils/server-status-context';
|
||||
import AlertMessageProvider from '../../utils/alert-message-context';
|
||||
import MainLayout from '../../components/main-layout';
|
||||
import MainLayout from '../main-layout';
|
||||
|
||||
function AdminLayout({ Component, pageProps }: AppProps) {
|
||||
return (
|
|
@ -130,7 +130,7 @@ export default function MainLayout(props) {
|
|||
<Sider width={240} className="side-nav">
|
||||
<h1 className="owncast-title">
|
||||
<span className="logo-container">
|
||||
<OwncastLogo />
|
||||
<OwncastLogo variant="simple" />
|
||||
</span>
|
||||
<span className="title-label">Owncast Admin</span>
|
||||
</h1>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
interface Props {}
|
||||
|
||||
export default function AuthModal(props: Props) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
interface Props {}
|
||||
|
||||
export default function BrowserNotifyModal(props: Props) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
interface Props {}
|
||||
|
||||
export default function FediAuthModal(props: Props) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
interface Props {}
|
||||
|
||||
export default function FollowModal(props: Props) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
interface Props {}
|
||||
|
||||
export default function IndieAuthModal(props: Props) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Card, Col, Row, Typography } from 'antd';
|
|||
import Link from 'next/link';
|
||||
import { useContext } from 'react';
|
||||
import LogTable from './log-table';
|
||||
import OwncastLogo from './logo';
|
||||
import OwncastLogo from './common/Logo/Logo';
|
||||
import NewsFeed from './news-feed';
|
||||
import { ConfigDetails } from '../types/config-section';
|
||||
import { ServerStatusContext } from '../utils/server-status-context';
|
||||
|
@ -125,7 +125,7 @@ export default function Offline({ logs = [], config }: OfflineProps) {
|
|||
<Col span={12} offset={6}>
|
||||
<div className="offline-intro">
|
||||
<span className="logo">
|
||||
<OwncastLogo />
|
||||
<OwncastLogo variant="simple" />
|
||||
</span>
|
||||
<div>
|
||||
<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 WebsocketService from '../../services/websocket-service';
|
||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||
import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
|
||||
import {
|
||||
AppState,
|
||||
ChatState,
|
||||
|
@ -16,10 +15,10 @@ import {
|
|||
getChatVisibilityState,
|
||||
} from '../../interfaces/application-state';
|
||||
import {
|
||||
SocketEvent,
|
||||
ConnectedClientInfoEvent,
|
||||
MessageType,
|
||||
ChatEvent,
|
||||
SocketEvent,
|
||||
} from '../../interfaces/socket-events';
|
||||
import handleConnectedClientInfoMessage from './eventhandlers/connectedclientinfo';
|
||||
import handleChatMessage from './eventhandlers/handleChatMessage';
|
||||
|
@ -77,10 +76,8 @@ export function ClientConfigStore() {
|
|||
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
|
||||
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
||||
const [appState, setAppState] = useRecoilState<AppState>(appStateAtom);
|
||||
const [videoState, setVideoState] = useRecoilState<VideoState>(videoStateAtom);
|
||||
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
||||
const [websocketService, setWebsocketService] =
|
||||
useRecoilState<WebsocketService>(websocketServiceAtom);
|
||||
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
|
||||
|
||||
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) {
|
||||
console.log('connected client', message);
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mobileChat {
|
||||
display: block;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: calc(50vh - var(--header-h));
|
||||
display: block;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: calc(50vh - var(--header-h));
|
||||
}
|
||||
|
||||
.leftCol {
|
||||
display: grid;
|
||||
// -64px, which is the header
|
||||
grid-template-rows: 50vh calc(50vh - var(--header-h));
|
||||
display: grid;
|
||||
// -64px, which is the header
|
||||
grid-template-rows: 50vh calc(50vh - var(--header-h));
|
||||
}
|
||||
.lowerRow {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr var(--header-h);
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr var(--header-h);
|
||||
}
|
||||
|
||||
.pageContentSection {
|
||||
|
@ -32,10 +32,10 @@
|
|||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.mobileChat {
|
||||
display: none;
|
||||
}
|
||||
.root[data-columns='2'] {
|
||||
grid-template-columns: 1fr var(--chat-w);
|
||||
}
|
||||
.mobileChat {
|
||||
display: none;
|
||||
}
|
||||
.root[data-columns='2'] {
|
||||
grid-template-columns: 1fr var(--chat-w);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { Layout, Button, Col, Tabs } from 'antd';
|
||||
import Grid from 'antd/lib/card/Grid';
|
||||
import { Layout, Button, Tabs } from 'antd';
|
||||
import {
|
||||
chatVisibilityAtom,
|
||||
clientConfigStateAtom,
|
||||
|
@ -23,6 +22,7 @@ import ActionButtonRow from '../../action-buttons/ActionButtonRow';
|
|||
import ActionButton from '../../action-buttons/ActionButton';
|
||||
import Statusbar from '../Statusbar/Statusbar';
|
||||
import { ServerStatus } from '../../../interfaces/server-status.model';
|
||||
import { Follower } from '../../../interfaces/follower';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
const { Content } = Layout;
|
||||
|
@ -34,8 +34,8 @@ export default function ContentComponent() {
|
|||
const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
|
||||
const chatState = useRecoilValue<ChatState>(chatStateAtom);
|
||||
|
||||
const { extraPageContent } = clientConfig;
|
||||
const { online, viewerCount, lastConnectTime, lastDisconnectTime, streamTitle } = status;
|
||||
const { extraPageContent, version } = clientConfig;
|
||||
const { online, viewerCount, lastConnectTime, lastDisconnectTime } = status;
|
||||
|
||||
const followers: Follower[] = [];
|
||||
|
||||
|
@ -88,7 +88,7 @@ export default function ContentComponent() {
|
|||
<ChatTextField />
|
||||
</div>
|
||||
)}
|
||||
<Footer />
|
||||
<Footer version={version} />
|
||||
</div>
|
||||
</div>
|
||||
{chatOpen && <Sidebar />}
|
||||
|
|
|
@ -1 +1 @@
|
|||
export { default } from "./Content"
|
||||
export { default } from './Content';
|
||||
|
|
|
@ -54,11 +54,11 @@ export default function CrossfadeImage({
|
|||
return (
|
||||
<span style={spanStyle}>
|
||||
{[...srcs, nextSrc].map(
|
||||
(src, index) =>
|
||||
src !== '' && (
|
||||
(singleSrc, index) =>
|
||||
singleSrc !== '' && (
|
||||
<img
|
||||
key={(key + index) % 3}
|
||||
src={src}
|
||||
key={singleSrc}
|
||||
src={singleSrc}
|
||||
alt=""
|
||||
style={imgStyles[index]}
|
||||
onLoad={index === 2 ? onLoadImg : undefined}
|
||||
|
|
|
@ -2,7 +2,11 @@ import { Layout } from 'antd';
|
|||
|
||||
const { Footer } = Layout;
|
||||
|
||||
export default function FooterComponent(props) {
|
||||
interface Props {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export default function FooterComponent(props: Props) {
|
||||
const { version } = props;
|
||||
|
||||
return <Footer style={{ textAlign: 'center', height: '64px' }}>Footer: Owncast {version}</Footer>;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
padding: .5rem 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Layout } from 'antd';
|
||||
import { ChatState } from '../../../interfaces/application-state';
|
||||
import { OwncastLogo, UserDropdown } from '../../common';
|
||||
import s from './Header.module.scss';
|
||||
|
||||
|
@ -12,10 +13,10 @@ export default function HeaderComponent({ name = 'Your stream title' }: Props) {
|
|||
return (
|
||||
<Header className={`${s.header}`}>
|
||||
<div className={`${s.logo}`}>
|
||||
<OwncastLogo variant='contrast'/>
|
||||
<OwncastLogo variant="contrast" />
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
<UserDropdown />
|
||||
<UserDropdown username="fillmein" chatState={ChatState.Available} />
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ export default function Modal(props: Props) {
|
|||
width="100%"
|
||||
height="100%"
|
||||
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
||||
allowpaymentrequest="true"
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
onLoad={() => setLoading(false)}
|
||||
|
|
|
@ -8,5 +8,3 @@
|
|||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ export default function Statusbar(props: Props) {
|
|||
const duration = makeDurationString(new Date(lastConnectTime));
|
||||
onlineMessage = online ? `Live for ${duration}` : 'Offline';
|
||||
rightSideMessage = `${viewerCount > 0 ? `${viewerCount}` : 'No'} ${
|
||||
viewerCount == 1 ? 'viewer' : 'viewers'
|
||||
viewerCount === 1 ? 'viewer' : 'viewers'
|
||||
}`;
|
||||
} else {
|
||||
onlineMessage = 'Offline';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export { default as Header } from './Header/index'
|
||||
export { default as Sidebar } from './Sidebar/index'
|
||||
export { default as Footer } from './Footer/index'
|
||||
export { default as Content } from './Content/index'
|
||||
export { default as Header } from './Header/index';
|
||||
export { default as Sidebar } from './Sidebar/index';
|
||||
export { default as Footer } from './Footer/index';
|
||||
export { default as Content } from './Content/index';
|
||||
|
|
|
@ -107,11 +107,7 @@ export default function OwncastPlayer(props: Props) {
|
|||
<div style={{ display: 'grid' }}>
|
||||
{online && (
|
||||
<div style={{ gridColumn: 1, gridRow: 1 }}>
|
||||
<VideoJS
|
||||
style={{ gridColumn: 1, gridRow: 1 }}
|
||||
options={videoJsOptions}
|
||||
onReady={handlePlayerReady}
|
||||
/>
|
||||
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{ gridColumn: 1, gridRow: 1 }}>
|
||||
|
|
|
@ -7,8 +7,12 @@ require('video.js/dist/video-js.css');
|
|||
// TODO: Restore volume that was saved in local storage.
|
||||
// import { getLocalStorage, setLocalStorage } from '../../utils/helpers.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 playerRef = React.useRef(null);
|
||||
const { options, onReady } = props;
|
||||
|
@ -18,11 +22,10 @@ export function VideoJS(props) {
|
|||
if (!playerRef.current) {
|
||||
const videoElement = videoRef.current;
|
||||
|
||||
// if (!videoElement) return;
|
||||
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
const player = (playerRef.current = videojs(videoElement, options, () => {
|
||||
player.log('player is ready');
|
||||
onReady && onReady(player);
|
||||
return onReady && onReady(player);
|
||||
}));
|
||||
|
||||
// TODO: Add airplay support, video settings menu, latency compensator, etc.
|
||||
|
@ -48,6 +51,7 @@ export function VideoJS(props) {
|
|||
|
||||
return (
|
||||
<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}`} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
# Tips for creating a new Admin form
|
||||
|
||||
### Layout
|
||||
|
||||
- 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.
|
||||
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
- `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.
|
||||
|
@ -22,24 +22,28 @@
|
|||
```
|
||||
|
||||
### 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.
|
||||
|
||||
- See `reset-yp.tsx` for an example of using `submitStatus` with `useState()` and the `<FormStatusIndicator>` component to achieve this.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# Creating Admin forms the Config section
|
||||
|
||||
First things first..
|
||||
|
||||
## 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.
|
||||
|
||||
|
@ -47,36 +51,39 @@ 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.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
3. Because ths config data is populated asynchronously, Use a `useEffect` to check when that data has arrived before putting it into state.
|
||||
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).
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
3. Because ths config data is populated asynchronously, Use a `useEffect` to check when that data has arrived before putting it into state.
|
||||
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).
|
||||
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.
|
||||
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.
|
||||
|
||||
- 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:
|
||||
|
||||
- `edit-instance-details.tsx`
|
||||
- `edit-server-details.tsx`
|
||||
|
||||
Examples of Config form groups where there is 1 submit button for the entire group include:
|
||||
|
||||
- `edit-storage.tsx`
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### 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.
|
||||
- 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.
|
||||
|
@ -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)
|
||||
|
||||
- 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 {
|
||||
name: string;
|
||||
title?: string;
|
||||
summary: string;
|
||||
logo: string;
|
||||
tags: string[];
|
||||
|
|
|
@ -3,4 +3,6 @@ export interface ExternalAction {
|
|||
description?: string;
|
||||
color?: 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",
|
||||
"@emoji-mart/data": "^1.0.1",
|
||||
"@storybook/react": "^6.4.22",
|
||||
"antd": "4.18.9",
|
||||
"antd": "^4.20.4",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"chart.js": "3.7.0",
|
||||
"chartkick": "4.1.1",
|
||||
|
@ -85,7 +85,7 @@
|
|||
"html-webpack-plugin": "^5.5.0",
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^10.2.0",
|
||||
"prettier": "2.5.1",
|
||||
"prettier": "2.6.2",
|
||||
"sass": "^1.50.0",
|
||||
"sass-loader": "^10.1.1",
|
||||
"sb": "^6.4.22",
|
||||
|
|
|
@ -21,13 +21,13 @@ import '../styles/pages.scss';
|
|||
import '../styles/offline-notice.scss';
|
||||
|
||||
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';
|
||||
|
||||
function App({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
const router = useRouter() as Router;
|
||||
if (router.pathname.startsWith('/admin')) {
|
||||
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 { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import { UpdateArgs } from '../../types/config-section';
|
||||
import isValidUrl from '../utils/urls';
|
||||
import isValidUrl from '../../utils/urls';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"extends": ["config:base"],
|
||||
"timezone": "America/Los_Angeles",
|
||||
"schedule": [
|
||||
"before 8am on Monday"
|
||||
],
|
||||
"schedule": ["before 8am on Monday"],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"automerge": true
|
||||
|
@ -15,37 +11,26 @@
|
|||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": [
|
||||
"minor"
|
||||
],
|
||||
"matchUpdateTypes": ["minor"],
|
||||
"matchCurrentVersion": "!/^0/",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchDepTypes": [
|
||||
"devDependencies"
|
||||
],
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"automerge": true,
|
||||
"major": {
|
||||
"dependencyDashboardApproval": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"*"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch"
|
||||
],
|
||||
"matchPackagePatterns": ["*"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"major": {
|
||||
"dependencyDashboardApproval": true
|
||||
},
|
||||
"groupName": "all non-major dependencies",
|
||||
"groupSlug": "all-minor-patch",
|
||||
"labels": [
|
||||
"dependencies"
|
||||
]
|
||||
"labels": ["dependencies"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import ServerStatus from '../interfaces/server-status.model';
|
||||
import { ServerStatus } from '../interfaces/server-status.model';
|
||||
|
||||
const ENDPOINT = `/api/status`;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { message } from 'antd';
|
||||
import { MessageType } from '../interfaces/socket-events';
|
||||
import { MessageType, SocketEvent } from '../interfaces/socket-events';
|
||||
|
||||
interface SocketMessage {
|
||||
export interface SocketMessage {
|
||||
type: MessageType;
|
||||
data: any;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export default class WebsocketService {
|
|||
|
||||
websocketReconnectTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
handleMessage?: (message: SocketMessage) => void;
|
||||
handleMessage?: (message: SocketEvent) => void;
|
||||
|
||||
constructor(accessToken, path) {
|
||||
this.accessToken = accessToken;
|
||||
|
@ -76,7 +76,7 @@ export default class WebsocketService {
|
|||
// Optimization where multiple events can be sent within a
|
||||
// single websocket message. So split them if needed.
|
||||
const messages = e.data.split('\n');
|
||||
let message: SocketMessage;
|
||||
let message: SocketEvent;
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
|
|
|
@ -10,9 +10,10 @@ export default {
|
|||
} as ComponentMeta<typeof ActionButtonRow>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const Template: ComponentStory<typeof ActionButtonRow> = args => (
|
||||
<ActionButtonRow>{args.buttons}</ActionButtonRow>
|
||||
);
|
||||
const Template: ComponentStory<typeof ActionButtonRow> = args => {
|
||||
const { buttons } = args as any;
|
||||
return <ActionButtonRow>{buttons}</ActionButtonRow>;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const actions = [
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import BrowserNotifyModal from '../components/modals/BrowserNotifyModal';
|
||||
import AuthModal from '../components/modals/AuthModal';
|
||||
|
||||
const Example = () => (
|
||||
<div>
|
||||
|
|
|
@ -9,7 +9,7 @@ export default {
|
|||
} as ComponentMeta<typeof ChatActionMessage>;
|
||||
|
||||
// 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
|
||||
export const Basic = Template.bind({});
|
||||
|
|
|
@ -9,7 +9,7 @@ export default {
|
|||
} as ComponentMeta<typeof ChatSocialMessage>;
|
||||
|
||||
// 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
|
||||
export const Basic = Template.bind({});
|
||||
|
|
|
@ -8,8 +8,7 @@ export default {
|
|||
parameters: {},
|
||||
} as ComponentMeta<typeof ChatSystemMessage>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const Template: ComponentStory<typeof ChatSystemMessage> = args => <ChatSystemMessage />;
|
||||
const Template: ComponentStory<typeof ChatSystemMessage> = args => <ChatSystemMessage {...args} />;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const Basic = Template.bind({});
|
||||
|
|
|
@ -25,7 +25,6 @@ export function Color(props) {
|
|||
|
||||
const colorDescriptionStyle = {
|
||||
margin: '5px',
|
||||
textAlign: 'center',
|
||||
color: 'gray',
|
||||
fontSize: '0.8em',
|
||||
};
|
||||
|
@ -33,7 +32,9 @@ export function Color(props) {
|
|||
return (
|
||||
<figure style={containerStyle}>
|
||||
<div style={colorBlockStyle} />
|
||||
<figcaption style={colorDescriptionStyle}>{color}</figcaption>
|
||||
<figcaption>
|
||||
<span style={colorDescriptionStyle}>{color}</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
@ -44,8 +45,8 @@ Color.propTypes = {
|
|||
|
||||
const rowStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row' as 'row',
|
||||
flexWrap: 'wrap' as 'wrap',
|
||||
// justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
TreeSelect,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import { SizeType } from 'antd/lib/config-provider/SizeContext';
|
||||
|
||||
const FormExample = () => {
|
||||
const [componentSize, setComponentSize] = useState('default');
|
||||
|
@ -33,7 +34,7 @@ const FormExample = () => {
|
|||
size: componentSize,
|
||||
}}
|
||||
onValuesChange={onFormLayoutChange}
|
||||
size={componentSize}
|
||||
size={componentSize as SizeType}
|
||||
>
|
||||
<Form.Item label="Form Size" name="size">
|
||||
<Radio.Group>
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
export function ImageAsset(props) {
|
||||
export function ImageAsset(props: ImageAssetProps) {
|
||||
const { name, src } = props;
|
||||
|
||||
const containerStyle = {
|
||||
|
@ -23,7 +21,7 @@ export function ImageAsset(props) {
|
|||
};
|
||||
|
||||
const colorDescriptionStyle = {
|
||||
textAlign: 'center',
|
||||
textAlign: 'center' as 'center',
|
||||
color: 'gray',
|
||||
fontSize: '0.8em',
|
||||
};
|
||||
|
@ -48,19 +46,20 @@ export function ImageAsset(props) {
|
|||
);
|
||||
}
|
||||
|
||||
Image.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
interface ImageAssetProps {
|
||||
name: string;
|
||||
src: string;
|
||||
}
|
||||
|
||||
const rowStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row' as 'row',
|
||||
flexWrap: 'wrap' as 'wrap',
|
||||
// justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
export function ImageRow(props) {
|
||||
export function ImageRow(props: ImageRowProps) {
|
||||
const { images } = props;
|
||||
|
||||
return (
|
||||
|
@ -72,6 +71,6 @@ export function ImageRow(props) {
|
|||
);
|
||||
}
|
||||
|
||||
ImageRow.propTypes = {
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
interface ImageRowProps {
|
||||
images: ImageAssetProps[];
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import {Color, ColorRow} from './Color';
|
||||
import {Image, ImageRow} from './ImageAsset';
|
||||
import { Color, ColorRow } from './Color';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
import Logo from '../assets/images/logo.svg';
|
||||
import FediverseColor from '../assets/images/fediverse-color.png';
|
||||
|
@ -16,73 +16,113 @@ import IsBot from '../assets/images/bot.svg';
|
|||
|
||||
`}</style>
|
||||
|
||||
export const images = [{
|
||||
src: Logo,
|
||||
name: 'Logo',
|
||||
}, {
|
||||
src: FediverseColor,
|
||||
name: 'Fediverse Color',
|
||||
},{
|
||||
src: FediverseBlack,
|
||||
name: 'Fediverse Black',
|
||||
}, {
|
||||
src: Moderator,
|
||||
name: 'Moderator',
|
||||
}, {
|
||||
src: IndieAuth,
|
||||
name: 'IndieAuth',
|
||||
}, {
|
||||
src: IsBot,
|
||||
name: 'Bot Flag',
|
||||
}];
|
||||
|
||||
export const images = [
|
||||
{
|
||||
src: Logo,
|
||||
name: 'Logo',
|
||||
},
|
||||
{
|
||||
src: FediverseColor,
|
||||
name: 'Fediverse Color',
|
||||
},
|
||||
{
|
||||
src: FediverseBlack,
|
||||
name: 'Fediverse Black',
|
||||
},
|
||||
{
|
||||
src: Moderator,
|
||||
name: 'Moderator',
|
||||
},
|
||||
{
|
||||
src: IndieAuth,
|
||||
name: 'IndieAuth',
|
||||
},
|
||||
{
|
||||
src: IsBot,
|
||||
name: 'Bot Flag',
|
||||
},
|
||||
];
|
||||
|
||||
# Colors
|
||||
|
||||
<Story name="Colors">
|
||||
</Story>
|
||||
<Story name="Colors"></Story>
|
||||
|
||||
<ColorRow colors={['theme-primary-color', 'theme-text-color-secondary']} />
|
||||
|
||||
## Text
|
||||
|
||||
<ColorRow colors={['theme-text-color', 'theme-text-color-secondary', 'theme-link-color']} />
|
||||
|
||||
## Backgrounds
|
||||
|
||||
<ColorRow colors={['theme-background', 'theme-background-secondary', 'popover-background']} />
|
||||
|
||||
## 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
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
|
||||
[Inter font](https://rsms.me/inter/)
|
||||
|
||||
<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')}
|
||||
</Canvas>
|
||||
</Story>
|
||||
|
||||
# Images
|
||||
|
||||
<Story name="Images and Icons">
|
||||
</Story>
|
||||
<Story name="Images and Icons"></Story>
|
||||
|
||||
<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,6 +1,7 @@
|
|||
# Style Definitions
|
||||
|
||||
Read more about [Style Dictionary](https://amzn.github.io/style-dictionary)
|
||||
|
||||
## Add
|
||||
|
||||
Add to the `tokens/**/*.yaml` files to add or modify style values.
|
||||
|
|
|
@ -3,32 +3,32 @@
|
|||
# https://github.com/ant-design/ant-design/blob/master/components/style/themes/dark.less
|
||||
|
||||
text-color:
|
||||
value: "var(--theme-text-color)"
|
||||
value: 'var(--theme-text-color)'
|
||||
text-color-secondry:
|
||||
value: "var(--theme-text-color-secondary)"
|
||||
value: 'var(--theme-text-color-secondary)'
|
||||
link-color:
|
||||
value: "var(--theme-link-color)"
|
||||
value: 'var(--theme-link-color)'
|
||||
popover-background:
|
||||
value: "var(--theme-background)"
|
||||
value: 'var(--theme-background)'
|
||||
background-color-light:
|
||||
value: "var(--theme-background-secondary)"
|
||||
value: 'var(--theme-background-secondary)'
|
||||
|
||||
# These values require explicit colors and cannot take css variables.
|
||||
primary-color:
|
||||
value: "{color.owncast.purple.500.value}"
|
||||
value: '{color.owncast.purple.500.value}'
|
||||
info-color:
|
||||
value: "{color.owncast.gray.500.value}"
|
||||
value: '{color.owncast.gray.500.value}'
|
||||
success-color:
|
||||
value: "{color.owncast.green.500.value}"
|
||||
value: '{color.owncast.green.500.value}'
|
||||
warning-color:
|
||||
value: "{color.owncast.orange.500.value}"
|
||||
value: '{color.owncast.orange.500.value}'
|
||||
error-color:
|
||||
value: "{color.owncast.red.500.value}"
|
||||
value: '{color.owncast.red.500.value}'
|
||||
purple-base:
|
||||
value: "{color.owncast.purple.500.value}"
|
||||
value: '{color.owncast.purple.500.value}'
|
||||
green-base:
|
||||
value: "{color.owncast.green.500.value}"
|
||||
value: '{color.owncast.green.500.value}'
|
||||
red-base:
|
||||
value: "{color.owncast.red.500.value}"
|
||||
value: '{color.owncast.red.500.value}'
|
||||
orange-base:
|
||||
value: "{color.owncast.orange.500.value}"
|
||||
value: '{color.owncast.orange.500.value}'
|
||||
|
|
|
@ -5,32 +5,32 @@
|
|||
|
||||
theme:
|
||||
primary-color:
|
||||
value: "{color.owncast.purple.500.value}"
|
||||
comment: "The primary color of the application used for rendering controls."
|
||||
value: '{color.owncast.purple.500.value}'
|
||||
comment: 'The primary color of the application used for rendering controls.'
|
||||
text-color:
|
||||
value: "{color.owncast.gray.300.value}"
|
||||
comment: "The color of the text in the application."
|
||||
value: '{color.owncast.gray.300.value}'
|
||||
comment: 'The color of the text in the application.'
|
||||
text-color-secondary:
|
||||
value: "{color.owncast.gray.500.value}"
|
||||
value: '{color.owncast.gray.500.value}'
|
||||
link-color:
|
||||
value: "{color.owncast.purple.500.value}"
|
||||
value: '{color.owncast.purple.500.value}'
|
||||
font-family:
|
||||
value: "{font.owncast.family.value}"
|
||||
value: '{font.owncast.family.value}'
|
||||
background:
|
||||
value: "{color.owncast.background.value}"
|
||||
comment: "The main background color of the page."
|
||||
value: '{color.owncast.background.value}'
|
||||
comment: 'The main background color of the page.'
|
||||
background-secondary:
|
||||
value: "{color.owncast.background-secondary.value}"
|
||||
comment: "A secondary background color used in sections and controls."
|
||||
value: '{color.owncast.background-secondary.value}'
|
||||
comment: 'A secondary background color used in sections and controls.'
|
||||
rounded-corners:
|
||||
value: "5px"
|
||||
comment: "The radius of rounded corners used in places."
|
||||
value: '5px'
|
||||
comment: 'The radius of rounded corners used in places.'
|
||||
|
||||
success-color:
|
||||
value: "{color.owncast.green.500.value}"
|
||||
value: '{color.owncast.green.500.value}'
|
||||
info-color:
|
||||
value: "{color.owncast.purple.300.value}"
|
||||
value: '{color.owncast.purple.300.value}'
|
||||
warning-color:
|
||||
value: "{color.owncast.orange.500.value}"
|
||||
value: '{color.owncast.orange.500.value}'
|
||||
error-color:
|
||||
value: "{color.owncast.red.500.value}"
|
||||
value: '{color.owncast.red.500.value}'
|
||||
|
|
|
@ -4,76 +4,76 @@ color:
|
|||
owncast:
|
||||
purple:
|
||||
100:
|
||||
value: "#f4ebff"
|
||||
value: '#f4ebff'
|
||||
300:
|
||||
value: "#d6bbfb"
|
||||
value: '#d6bbfb'
|
||||
500:
|
||||
value: "#9e77ed"
|
||||
value: '#9e77ed'
|
||||
700:
|
||||
value: "#6941c6"
|
||||
value: '#6941c6'
|
||||
900:
|
||||
value: "#42307d"
|
||||
value: '#42307d'
|
||||
|
||||
green:
|
||||
100:
|
||||
value: "#d15ad5"
|
||||
value: '#d15ad5'
|
||||
300:
|
||||
value: "#6ce9a6"
|
||||
value: '#6ce9a6'
|
||||
500:
|
||||
value: "#12b76a"
|
||||
value: '#12b76a'
|
||||
700:
|
||||
value: "#027a48"
|
||||
value: '#027a48'
|
||||
900:
|
||||
value: "#054f31"
|
||||
value: '#054f31'
|
||||
|
||||
red:
|
||||
100:
|
||||
value: "#fee4e2"
|
||||
value: '#fee4e2'
|
||||
300:
|
||||
value: "#fda29b"
|
||||
value: '#fda29b'
|
||||
500:
|
||||
value: "#f04438"
|
||||
value: '#f04438'
|
||||
700:
|
||||
value: "#b42318"
|
||||
value: '#b42318'
|
||||
900:
|
||||
value: "#7a271a"
|
||||
value: '#7a271a'
|
||||
|
||||
orange:
|
||||
100:
|
||||
value: "#fef0c7"
|
||||
value: '#fef0c7'
|
||||
300:
|
||||
value: "#fec84b"
|
||||
value: '#fec84b'
|
||||
500:
|
||||
value: "#f79009"
|
||||
value: '#f79009'
|
||||
700:
|
||||
value: "#b54708"
|
||||
value: '#b54708'
|
||||
900:
|
||||
value: "#93370d"
|
||||
value: '#93370d'
|
||||
|
||||
gray:
|
||||
100:
|
||||
value: "#f2f4f7"
|
||||
value: '#f2f4f7'
|
||||
300:
|
||||
value: "#d0d5dd"
|
||||
value: '#d0d5dd'
|
||||
500:
|
||||
value: "#667085"
|
||||
value: '#667085'
|
||||
700:
|
||||
value: "#344054"
|
||||
value: '#344054'
|
||||
900:
|
||||
value: "#101828"
|
||||
value: '#101828'
|
||||
|
||||
logo:
|
||||
purple:
|
||||
value: "rgba(120, 113, 255, 1)"
|
||||
value: 'rgba(120, 113, 255, 1)'
|
||||
pink:
|
||||
value: "rgba(201, 139, 254, 1)"
|
||||
value: 'rgba(201, 139, 254, 1)'
|
||||
blue:
|
||||
value: "rgba(32, 134, 225, 1)"
|
||||
value: 'rgba(32, 134, 225, 1)'
|
||||
|
||||
background:
|
||||
value: "rgba(27, 26, 38, 1)"
|
||||
value: 'rgba(27, 26, 38, 1)'
|
||||
background-secondary:
|
||||
value: "rgba(22, 21, 31, 1)"
|
||||
value: 'rgba(22, 21, 31, 1)'
|
||||
|
||||
font:
|
||||
owncast:
|
||||
|
@ -85,28 +85,28 @@ font:
|
|||
# Values used in the admin and should be migrated to variables or removed.
|
||||
# See ant-overrides.scss.
|
||||
owncast-purple:
|
||||
value: "{color.owncast.logo.purple.value}"
|
||||
value: '{color.owncast.logo.purple.value}'
|
||||
owncast-purple-25:
|
||||
value: "rgba(120, 113, 255, 0.25)"
|
||||
value: 'rgba(120, 113, 255, 0.25)'
|
||||
owncast-purple-50:
|
||||
value: "rgba(120, 113, 255, 0.5)"
|
||||
value: 'rgba(120, 113, 255, 0.5)'
|
||||
online-color:
|
||||
value: "#73dd3f"
|
||||
value: '#73dd3f'
|
||||
offline-color:
|
||||
value: "#999"
|
||||
value: '#999'
|
||||
pink:
|
||||
value: "{color.owncast.logo.pink.value}"
|
||||
value: '{color.owncast.logo.pink.value}'
|
||||
purple:
|
||||
value: "{color.owncast.purple.500.value}"
|
||||
value: '{color.owncast.purple.500.value}'
|
||||
blue:
|
||||
value: "{color.owncast.logo.blue.value}"
|
||||
value: '{color.owncast.logo.blue.value}'
|
||||
white-88:
|
||||
value: "{color.owncast.gray.500.value}"
|
||||
value: '{color.owncast.gray.500.value}'
|
||||
purple-dark:
|
||||
value: "{color.owncast.purple.900.value}"
|
||||
value: '{color.owncast.purple.900.value}'
|
||||
default-link-color:
|
||||
value: "{color.owncast.purple.700.value}"
|
||||
value: '{color.owncast.purple.700.value}'
|
||||
default-bg-color:
|
||||
value: "{color.owncast.background.value}"
|
||||
value: '{color.owncast.background.value}'
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// SELECT
|
||||
.ant-select-dropdown {
|
||||
background-color: var(--black);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// styles for Storage config section
|
||||
|
||||
|
||||
.edit-storage-container {
|
||||
padding: 1em;
|
||||
.form-fields {
|
||||
|
@ -21,9 +20,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.edit-server-details-container {
|
||||
|
||||
// Do something special for the stream key field
|
||||
.field-streamkey-container {
|
||||
margin-bottom: 1.5em;
|
||||
|
@ -43,7 +40,7 @@
|
|||
.streamkey-actions {
|
||||
white-space: nowrap;
|
||||
button {
|
||||
margin: .25em;
|
||||
margin: 0.25em;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
margin-top: 2em;
|
||||
|
@ -54,5 +51,4 @@
|
|||
.advanced-settings {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
}
|
|
@ -19,40 +19,37 @@
|
|||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-size: .75rem;
|
||||
font-size: 0.75rem;
|
||||
.status-icon {
|
||||
display: inline-block;
|
||||
margin-right: .5em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* TIP CONTAINER BASE */
|
||||
.field-tip {
|
||||
font-size: .8em;
|
||||
font-size: 0.8em;
|
||||
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.
|
||||
*/
|
||||
.field-container {
|
||||
padding: .85em 0 .5em;
|
||||
padding: 0.85em 0 0.5em;
|
||||
}
|
||||
|
||||
|
||||
/* SEGMENT SLIDER GROUP WITH SELECTED NOTE, OR STATUS */
|
||||
.segment-slider-container {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
padding: 1em 2em .75em;
|
||||
padding: 1em 2em 0.75em;
|
||||
background-color: var(--owncast-purple-25);
|
||||
border-radius: var(--container-border-radius);
|
||||
|
||||
.status-container {
|
||||
width: 100%;
|
||||
margin: .5em auto;
|
||||
margin: 0.5em auto;
|
||||
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%;
|
||||
margin: 3em auto 0;
|
||||
text-align: center;
|
||||
font-size: .75em;
|
||||
font-size: 0.75em;
|
||||
line-height: normal;
|
||||
color: var(--white);
|
||||
padding: 1em;
|
||||
|
@ -69,7 +66,6 @@ Ideal for wrapping each Textfield on a page with many text fields in a row. This
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.segment-tip {
|
||||
width: 10em;
|
||||
text-align: center;
|
||||
|
|
|
@ -19,7 +19,7 @@ Base styles for
|
|||
padding-right: 1.25em;
|
||||
text-align: right;
|
||||
width: var(--form-label-container-width);
|
||||
margin: .2em 0;
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
.formfield-label {
|
||||
font-weight: 500;
|
||||
|
@ -35,7 +35,7 @@ Base styles for
|
|||
&::before {
|
||||
content: '*';
|
||||
display: inline-block;
|
||||
margin-right: .25em;
|
||||
margin-right: 0.25em;
|
||||
color: var(--ant-error);
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ Base styles for
|
|||
}
|
||||
|
||||
.status-container {
|
||||
margin: .25em;
|
||||
margin: 0.25em;
|
||||
width: 100%;
|
||||
display: block;
|
||||
&.empty {
|
||||
|
@ -64,7 +64,7 @@ Base styles for
|
|||
}
|
||||
|
||||
.field-tip {
|
||||
margin: .5em .5em;
|
||||
margin: 0.5em 0.5em;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
|
@ -117,7 +117,7 @@ Base styles for
|
|||
width: 100%;
|
||||
}
|
||||
.status-container {
|
||||
margin: .5em;
|
||||
margin: 0.5em;
|
||||
&.empty {
|
||||
display: none;
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ Base styles for
|
|||
}
|
||||
.update-button-container {
|
||||
visibility: hidden;
|
||||
margin: .25em 0;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,7 +137,6 @@ Base styles for
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.label-spacer {
|
||||
display: none;
|
||||
|
@ -145,7 +144,6 @@ Base styles for
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/* TOGGLE SWITCH CONTAINER BASE */
|
||||
.toggleswitch-container {
|
||||
margin: 2em 0 1em;
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
// --header-h: 64px;
|
||||
--chat-w: 300px;
|
||||
--chat-input-h: 40.5px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// misc styling for various /pages
|
||||
|
||||
|
||||
// .help-page {
|
||||
// .ant-result-image {
|
||||
// height: 100px;
|
||||
|
@ -11,9 +10,9 @@
|
|||
// }
|
||||
// }
|
||||
|
||||
|
||||
.upgrade-page {
|
||||
h2,h3 {
|
||||
h2,
|
||||
h3 {
|
||||
color: var(--pink);
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
// Do not edit directly
|
||||
// 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-secondary: #667085;
|
||||
@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-secondary: #16151f; // A secondary background color used in sections and controls.
|
||||
@theme-rounded-corners: 5px; // The radius of rounded corners.
|
||||
|
@ -58,7 +59,9 @@
|
|||
@color-owncast-logo-blue: #2086e1;
|
||||
@color-owncast-background: #1b1a26;
|
||||
@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-25: rgba(120, 113, 255, 0.25);
|
||||
@owncast-purple-50: rgba(120, 113, 255, 0.5);
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
|
@ -19,12 +15,6 @@
|
|||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
@ -19,13 +19,13 @@ export default function useWindowSize() {
|
|||
}
|
||||
|
||||
// Add event listener
|
||||
window.addEventListener("resize", handleResize);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Call handler right away so state gets updated with initial window size
|
||||
handleResize();
|
||||
|
||||
// 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
|
||||
|
||||
return windowSize;
|
||||
|
|
|
@ -5,11 +5,15 @@ export default function isValidUrl(url: string): boolean {
|
|||
const validProtocols = ['http:', 'https:'];
|
||||
|
||||
try {
|
||||
const validationObject = new URL(url);
|
||||
if (validationObject.protocol === '' || validationObject.hostname === '' || !validProtocols.includes(validationObject.protocol)) {
|
||||
return false;
|
||||
}
|
||||
} catch(e) {
|
||||
const validationObject = new URL(url);
|
||||
if (
|
||||
validationObject.protocol === '' ||
|
||||
validationObject.hostname === '' ||
|
||||
!validProtocols.includes(validationObject.protocol)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue