Fix web project build errors

This commit is contained in:
Gabe Kangas 2022-05-11 23:31:31 -07:00
parent b66617961d
commit 72c01e1b9a
No known key found for this signature in database
GPG key ID: 9A56337728BC81EA
86 changed files with 863 additions and 813 deletions

View file

@ -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:

View file

@ -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,
{ {

View file

@ -8,7 +8,7 @@ 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
@ -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. **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`

View file

@ -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 }} />;
} }

View file

@ -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>;
} }

View file

@ -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>;
} }

View file

@ -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 {

View file

@ -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>;
} }

View file

@ -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';

View file

@ -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) {

View file

@ -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 {

View file

@ -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 {

View file

@ -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);

View file

@ -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>

View file

@ -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(() => {

View file

@ -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.

View file

@ -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);
} }
} }

View file

@ -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.simple]: variant === 'simple',
[s.contrast]: variant === 'contrast', [s.contrast]: variant === 'contrast',
} });
)
return ( return (
<div className={rootClassName}> <div className={rootClassName}>

View file

@ -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>
); );

View file

@ -1 +1 @@
export { default } from './UserDropdown' export { default } from './UserDropdown';

View file

@ -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';

View file

@ -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 (

View file

@ -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>

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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>

View file

@ -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;

View file

@ -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);

View file

@ -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 />}

View file

@ -1 +1 @@
export { default } from "./Content" export { default } from './Content';

View file

@ -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}

View file

@ -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>;

View file

@ -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;

View file

@ -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>
); );
} }

View file

@ -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)}

View file

@ -8,5 +8,3 @@
display: flex; display: flex;
} }
} }

View file

@ -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';

View file

@ -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';

View file

@ -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 }}>

View file

@ -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>
); );

View file

@ -1,6 +1,7 @@
# 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.
@ -8,9 +9,8 @@
- 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.
@ -22,19 +22,23 @@
``` ```
### 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
@ -47,14 +51,14 @@ 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`. 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. 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. 3. Because ths config data is populated asynchronously, Use a `useEffect` to check when that data has arrived before putting it into state.
@ -68,15 +72,18 @@ There are also a variety of other local states to manage the display of error/su
- 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.
@ -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.

View file

@ -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[];

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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} />;
} }

View file

@ -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;

View file

@ -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"
]
} }
] ]
} }

View file

@ -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`;

View file

@ -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++) {

View file

@ -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 = [

View file

@ -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>

View file

@ -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({});

View file

@ -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({});

View file

@ -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({});

View file

@ -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',
}; };

View file

@ -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>

View file

@ -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[];
}; }

View file

@ -16,54 +16,100 @@ import IsBot from '../assets/images/bot.svg';
`}</style> `}</style>
export const images = [{ export const images = [
{
src: Logo, src: Logo,
name: 'Logo', name: 'Logo',
}, { },
{
src: FediverseColor, src: FediverseColor,
name: 'Fediverse Color', name: 'Fediverse Color',
},{ },
{
src: FediverseBlack, src: FediverseBlack,
name: 'Fediverse Black', name: 'Fediverse Black',
}, { },
{
src: Moderator, src: Moderator,
name: 'Moderator', name: 'Moderator',
}, { },
{
src: IndieAuth, src: IndieAuth,
name: 'IndieAuth', name: 'IndieAuth',
}, { },
{
src: IsBot, src: IsBot,
name: 'Bot Flag', 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
@ -77,12 +123,6 @@ export const images = [{
# Images # Images
<Story name="Images and Icons"> <Story name="Images and Icons"></Story>
</Story>
<ImageRow images={images} /> <ImageRow images={images} />

View file

@ -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: '',
};

View file

@ -1,6 +1,7 @@
# 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.

View file

@ -3,32 +3,32 @@
# 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}'

View file

@ -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}'

View file

@ -4,76 +4,76 @@ 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:
@ -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}'

View file

@ -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);

View file

@ -1,6 +1,5 @@
// styles for Storage config section // styles for Storage config section
.edit-storage-container { .edit-storage-container {
padding: 1em; padding: 1em;
.form-fields { .form-fields {
@ -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;
@ -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;
} }
} }

View file

@ -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;

View file

@ -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 {
@ -64,7 +64,7 @@ Base styles for
} }
.field-tip { .field-tip {
margin: .5em .5em; margin: 0.5em 0.5em;
} }
@media (max-width: 800px) { @media (max-width: 800px) {
@ -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,7 +144,6 @@ Base styles for
} }
} }
/* TOGGLE SWITCH CONTAINER BASE */ /* TOGGLE SWITCH CONTAINER BASE */
.toggleswitch-container { .toggleswitch-container {
margin: 2em 0 1em; margin: 2em 0 1em;

View file

@ -7,7 +7,6 @@
--chat-input-h: 40.5px; --chat-input-h: 40.5px;
} }
html, html,
body { body {
padding: 0; padding: 0;

View file

@ -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;
} }

View file

@ -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);

View file

@ -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"
]
} }

View file

@ -19,13 +19,13 @@ export default function useWindowSize() {
} }
// 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;

View file

@ -6,7 +6,11 @@ export default function isValidUrl(url: string): boolean {
try { try {
const validationObject = new URL(url); const validationObject = new URL(url);
if (validationObject.protocol === '' || validationObject.hostname === '' || !validProtocols.includes(validationObject.protocol)) { if (
validationObject.protocol === '' ||
validationObject.hostname === '' ||
!validProtocols.includes(validationObject.protocol)
) {
return false; return false;
} }
} catch (e) { } catch (e) {