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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
interface 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';
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';
interface Props {

View file

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

View file

@ -1,4 +1,4 @@
.root {
height: 2rem;
color: var(--black);
height: 2rem;
color: var(--black);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 OwncastLogo } from './Logo'
export { default as UserDropdown } from './UserDropdown';
export { default as OwncastLogo } from './Logo';

View file

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

View file

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

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
interface Props {}
export default function AuthModal(props: Props) {

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
interface Props {}
export default function BrowserNotifyModal(props: Props) {

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
interface Props {}
export default function FediAuthModal(props: Props) {

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
interface Props {}
export default function FollowModal(props: Props) {

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
interface 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 { 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>

View file

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

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) {
console.log('connected client', message);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
export interface ClientConfig {
name: string;
title?: string;
summary: string;
logo: string;
tags: string[];

View file

@ -3,4 +3,6 @@ export interface ExternalAction {
description?: string;
color?: 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",
"@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",

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import ServerStatus from '../interfaces/server-status.model';
import { ServerStatus } from '../interfaces/server-status.model';
const ENDPOINT = `/api/status`;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
Read more about [Style Dictionary](https://amzn.github.io/style-dictionary)
## Add
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
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}'

View file

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

View file

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

View file

@ -356,7 +356,6 @@ textarea.ant-input {
opacity: 0.75;
}
// SELECT
.ant-select-dropdown {
background-color: var(--black);

View file

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

View file

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

View file

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

View file

@ -5,8 +5,7 @@
// --header-h: 64px;
--chat-w: 300px;
--chat-input-h: 40.5px;
}
}
html,
body {

View file

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

View file

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

View file

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

View file

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

View file

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