reafctor: normalize component formatting (#2082)
* refactor: move/rename BanUserButton file * refactor: move/rename Chart file * refactor: update generic component filenames to PascalCase * refactor: update config component filenames to PascalCase * refactor: update AdminLayout component filename to PascalCase * refactor: update/move VideoJS component * chore(eslint): disable bad react/require-default-props rule * refactor: normalize ActionButton component * refactor: normalize ActionButtonRow component * refactor: normalize FollowButton component * refactor: normalize NotifyButton component * refactor: normalize ChatActionMessage component * refactor: normalize ChatContainer component * refactor: normalize ChatJoinMessage component * refactor: normalize ChatModerationActionMenu component * refactor: normalize ChatModerationDetailsModal component * refactor: normalize ChatModeratorNotification component * refactor: normalize ChatSocialMessage component * refactor: normalize ChatSystemMessage component * refactor: normalize ChatTextField component * refactor: normalize ChatUserBadge component * refactor: normalize ChatUserMessage component * refactor: normalize ContentHeader component * refactor: normalize OwncastLogo component * refactor: normalize UserDropdown component * chore(eslint): modify react/function-component-definition rule * refactor: normalize CodecSelector component * refactor: update a bunch of functional components using eslint * refactor: update a bunch of functional components using eslint, pt2 * refactor: update a bunch of functional components using eslint, pt3 * refactor: replace all component->component default imports with named imports * refactor: replace all component-stories->component default imports with named imports * refactor: remove default exports from most components * chore(eslint): add eslint config files for the components and pages dirs * fix: use-before-define error in ChatContainer * Fix ChatContainer import * Only process .tsx files in Next builds Co-authored-by: Gabe Kangas <gabek@real-ity.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Layout, Tabs, Spin } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { LOCAL_STORAGE_KEYS, getLocalStorage, setLocalStorage } from '../../../utils/localStorage';
|
||||
|
||||
@@ -17,32 +17,32 @@ import {
|
||||
serverStatusState,
|
||||
} from '../../stores/ClientConfigStore';
|
||||
import { ClientConfig } from '../../../interfaces/client-config.model';
|
||||
import CustomPageContent from '../CustomPageContent/CustomPageContent';
|
||||
import OwncastPlayer from '../../video/OwncastPlayer/OwncastPlayer';
|
||||
import FollowerCollection from '../followers/FollowerCollection/FollowerCollection';
|
||||
import s from './Content.module.scss';
|
||||
import Sidebar from '../Sidebar';
|
||||
import Footer from '../Footer';
|
||||
import { CustomPageContent } from '../CustomPageContent/CustomPageContent';
|
||||
import { OwncastPlayer } from '../../video/OwncastPlayer/OwncastPlayer';
|
||||
import { FollowerCollection } from '../followers/FollowerCollection/FollowerCollection';
|
||||
import styles from './Content.module.scss';
|
||||
import { Sidebar } from '../Sidebar/Sidebar';
|
||||
import { Footer } from '../Footer/Footer';
|
||||
// import ChatContainer from '../../chat/ChatContainer';
|
||||
// import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
// import ChatTextField from '../../chat/ChatTextField/ChatTextField';
|
||||
import ActionButtonRow from '../../action-buttons/ActionButtonRow/ActionButtonRow';
|
||||
import ActionButton from '../../action-buttons/ActionButton/ActionButton';
|
||||
import NotifyReminderPopup from '../NotifyReminderPopup/NotifyReminderPopup';
|
||||
import OfflineBanner from '../OfflineBanner/OfflineBanner';
|
||||
import { ActionButtonRow } from '../../action-buttons/ActionButtonRow/ActionButtonRow';
|
||||
import { ActionButton } from '../../action-buttons/ActionButton/ActionButton';
|
||||
import { NotifyReminderPopup } from '../NotifyReminderPopup/NotifyReminderPopup';
|
||||
import { OfflineBanner } from '../OfflineBanner/OfflineBanner';
|
||||
import { AppStateOptions } from '../../stores/application-state';
|
||||
import FollowButton from '../../action-buttons/FollowButton';
|
||||
import NotifyButton from '../../action-buttons/NotifyButton';
|
||||
import Modal from '../Modal/Modal';
|
||||
import BrowserNotifyModal from '../../modals/BrowserNotifyModal/BrowserNotifyModal';
|
||||
import ContentHeader from '../../common/ContentHeader';
|
||||
import { FollowButton } from '../../action-buttons/FollowButton';
|
||||
import { NotifyButton } from '../../action-buttons/NotifyButton';
|
||||
import { Modal } from '../Modal/Modal';
|
||||
import { BrowserNotifyModal } from '../../modals/BrowserNotifyModal/BrowserNotifyModal';
|
||||
import { ContentHeader } from '../../common/ContentHeader/ContentHeader';
|
||||
import { ServerStatus } from '../../../interfaces/server-status.model';
|
||||
import { StatusBar } from '..';
|
||||
import { Statusbar } from '../Statusbar/Statusbar';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
const { Content } = Layout;
|
||||
const { Content: AntContent } = Layout;
|
||||
|
||||
export default function ContentComponent() {
|
||||
export const Content: FC = () => {
|
||||
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
||||
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector);
|
||||
@@ -105,17 +105,17 @@ export default function ContentComponent() {
|
||||
window.addEventListener('resize', checkIfMobile);
|
||||
}, []);
|
||||
|
||||
const rootClassName = cn(s.root, {
|
||||
[s.mobile]: isMobile,
|
||||
const rootClassName = cn(styles.root, {
|
||||
[styles.mobile]: isMobile,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Content className={rootClassName}>
|
||||
<div className={s.leftContent}>
|
||||
<Spin className={s.loadingSpinner} size="large" spinning={appState.appLoading} />
|
||||
<AntContent className={rootClassName}>
|
||||
<div className={styles.leftContent}>
|
||||
<Spin className={styles.loadingSpinner} size="large" spinning={appState.appLoading} />
|
||||
|
||||
<div className={s.topSection}>
|
||||
<div className={styles.topSection}>
|
||||
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />}
|
||||
{!online && (
|
||||
<OfflineBanner
|
||||
@@ -125,15 +125,15 @@ export default function ContentComponent() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<StatusBar
|
||||
<Statusbar
|
||||
online={online}
|
||||
lastConnectTime={lastConnectTime}
|
||||
lastDisconnectTime={lastDisconnectTime}
|
||||
viewerCount={viewerCount}
|
||||
/>
|
||||
</div>
|
||||
<div className={s.midSection}>
|
||||
<div className={s.buttonsLogoTitleSection}>
|
||||
<div className={styles.midSection}>
|
||||
<div className={styles.buttonsLogoTitleSection}>
|
||||
<ActionButtonRow>
|
||||
{externalActionButtons}
|
||||
<FollowButton size="small" />
|
||||
@@ -156,7 +156,7 @@ export default function ContentComponent() {
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.lowerHalf}>
|
||||
<div className={styles.lowerHalf}>
|
||||
<ContentHeader
|
||||
name={name}
|
||||
title={streamTitle}
|
||||
@@ -167,7 +167,7 @@ export default function ContentComponent() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={s.lowerSection}>
|
||||
<div className={styles.lowerSection}>
|
||||
<Tabs defaultActiveKey="0" style={{ height: '100%' }}>
|
||||
{isChatVisible && isMobile && (
|
||||
<TabPane tab="Chat" key="0" style={{ height: '100%' }}>
|
||||
@@ -184,18 +184,19 @@ export default function ContentComponent() {
|
||||
</div> */}
|
||||
</TabPane>
|
||||
)}
|
||||
<TabPane tab="About" key="2" className={s.pageContentSection}>
|
||||
<TabPane tab="About" key="2" className={styles.pageContentSection}>
|
||||
<CustomPageContent content={extraPageContent} />
|
||||
</TabPane>
|
||||
<TabPane tab="Followers" key="3" className={s.pageContentSection}>
|
||||
<TabPane tab="Followers" key="3" className={styles.pageContentSection}>
|
||||
<FollowerCollection />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
{isChatVisible && !isMobile && <Sidebar />}
|
||||
</Content>
|
||||
</AntContent>
|
||||
{!isMobile && <Footer version={version} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default Content;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './Content';
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
|
||||
type ObjectFit = React.CSSProperties['objectFit'];
|
||||
|
||||
interface CrossfadeImageProps {
|
||||
export type CrossfadeImageProps = {
|
||||
src: string;
|
||||
width: string;
|
||||
height: string;
|
||||
objectFit?: ObjectFit;
|
||||
duration?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const imgStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
@@ -16,13 +16,13 @@ const imgStyle: React.CSSProperties = {
|
||||
height: `100%`,
|
||||
};
|
||||
|
||||
export default function CrossfadeImage({
|
||||
export const CrossfadeImage: FC<CrossfadeImageProps> = ({
|
||||
src = '',
|
||||
width,
|
||||
height,
|
||||
objectFit = 'fill',
|
||||
duration = '1s',
|
||||
}: CrossfadeImageProps) {
|
||||
}) => {
|
||||
const spanStyle: React.CSSProperties = useMemo(
|
||||
() => ({
|
||||
display: 'inline-block',
|
||||
@@ -67,7 +67,8 @@ export default function CrossfadeImage({
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default CrossfadeImage;
|
||||
|
||||
CrossfadeImage.defaultProps = {
|
||||
objectFit: 'fill',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import CustomPageContent from './CustomPageContent';
|
||||
import { CustomPageContent } from './CustomPageContent';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Components/Custom page content',
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
import s from './CustomPageContent.module.scss';
|
||||
import { FC } from 'react';
|
||||
import styles from './CustomPageContent.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type CustomPageContentProps = {
|
||||
content: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function CustomPageContent(props: Props) {
|
||||
const { content } = props;
|
||||
// eslint-disable-next-line react/no-danger
|
||||
return (
|
||||
<div className={s.pageContentContainer}>
|
||||
<div className={s.customPageContent} dangerouslySetInnerHTML={{ __html: content }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const CustomPageContent: FC<CustomPageContentProps> = ({ content }) => (
|
||||
<div className={styles.pageContentContainer}>
|
||||
<div className={styles.customPageContent} dangerouslySetInnerHTML={{ __html: content }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import Footer from './Footer';
|
||||
import { Footer } from './Footer';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Layout/Footer',
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
import s from './Footer.module.scss';
|
||||
import { FC } from 'react';
|
||||
import styles from './Footer.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type FooterProps = {
|
||||
version: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function FooterComponent(props: Props) {
|
||||
const { version } = props;
|
||||
|
||||
return (
|
||||
<div className={s.footer}>
|
||||
<div className={s.text}>
|
||||
Powered by <a href="https://owncast.online">{version}</a>
|
||||
export const Footer: FC<FooterProps> = ({ version }) => (
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.text}>
|
||||
Powered by <a href="https://owncast.online">{version}</a>
|
||||
</div>
|
||||
<div className={styles.links}>
|
||||
<div className={styles.item}>
|
||||
<a href="https://owncast.online/docs" target="_blank" rel="noreferrer">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<div className={s.links}>
|
||||
<div className={s.item}>
|
||||
<a href="https://owncast.online/docs" target="_blank" rel="noreferrer">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<div className={s.item}>
|
||||
<a href="https://owncast.online/help" target="_blank" rel="noreferrer">
|
||||
Contribute
|
||||
</a>
|
||||
</div>
|
||||
<div className={s.item}>
|
||||
<a href="https://github.com/owncast/owncast" target="_blank" rel="noreferrer">
|
||||
Source
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<a href="https://owncast.online/help" target="_blank" rel="noreferrer">
|
||||
Contribute
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<a href="https://github.com/owncast/owncast" target="_blank" rel="noreferrer">
|
||||
Source
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
export default Footer;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './Footer';
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import Header from './Header';
|
||||
import { Header } from './Header';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Layout/Header',
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import { Layout, Tag, Tooltip } from 'antd';
|
||||
import { OwncastLogo, UserDropdown } from '../../common';
|
||||
import s from './Header.module.scss';
|
||||
import { FC } from 'react';
|
||||
import { UserDropdown } from '../../common/UserDropdown/UserDropdown';
|
||||
import { OwncastLogo } from '../../common/OwncastLogo/OwncastLogo';
|
||||
import styles from './Header.module.scss';
|
||||
|
||||
const { Header } = Layout;
|
||||
const { Header: AntHeader } = Layout;
|
||||
|
||||
interface Props {
|
||||
export type HeaderComponentProps = {
|
||||
name: string;
|
||||
chatAvailable: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export default function HeaderComponent({ name = 'Your stream title', chatAvailable }: Props) {
|
||||
return (
|
||||
<Header className={`${s.header}`}>
|
||||
<div className={`${s.logo}`}>
|
||||
<OwncastLogo variant="contrast" />
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
{chatAvailable && <UserDropdown />}
|
||||
{!chatAvailable && (
|
||||
<Tooltip title="Chat is available when the stream is live." placement="left">
|
||||
<Tag color="processing" style={{ cursor: 'pointer' }}>
|
||||
Chat offline
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
export const Header: FC<HeaderComponentProps> = ({ name = 'Your stream title', chatAvailable }) => (
|
||||
<AntHeader className={`${styles.header}`}>
|
||||
<div className={`${styles.logo}`}>
|
||||
<OwncastLogo variant="contrast" />
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
{chatAvailable && <UserDropdown />}
|
||||
{!chatAvailable && (
|
||||
<Tooltip title="Chat is available when the stream is live." placement="left">
|
||||
<Tag color="processing" style={{ cursor: 'pointer' }}>
|
||||
Chat offline
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</AntHeader>
|
||||
);
|
||||
export default Header;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './Header';
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Image } from 'antd';
|
||||
import s from './Logo.module.scss';
|
||||
import { FC } from 'react';
|
||||
import styles from './Logo.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type LogoProps = {
|
||||
src: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function Logo({ src }: Props) {
|
||||
return (
|
||||
<div className={s.root}>
|
||||
<div className={s.container}>
|
||||
<Image src={src} alt="Logo" className={s.image} rootClassName={s.image} />
|
||||
</div>
|
||||
export const Logo: FC<LogoProps> = ({ src }) => (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.container}>
|
||||
<Image src={src} alt="Logo" className={styles.image} rootClassName={styles.image} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
export default Logo;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './Logo';
|
||||
@@ -1,31 +1,29 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
import { CSSProperties } from 'react';
|
||||
import { CSSProperties, FC } from 'react';
|
||||
|
||||
interface Props {
|
||||
export type ModIconProps = {
|
||||
style?: CSSProperties;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
}
|
||||
export default function ModIcon({
|
||||
};
|
||||
|
||||
export const ModIcon: FC<ModIconProps> = ({
|
||||
style = { width: '1rem', height: '1rem' },
|
||||
fill = 'none',
|
||||
stroke = 'var(--color-owncast-gray-300)',
|
||||
}: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
style={style}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>This user has moderation rights</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}: ModIconProps) => (
|
||||
<svg
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
style={style}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>This user has moderation rights</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import Modal from './Modal';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Modals/Container',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Spin, Skeleton, Modal as AntModal } from 'antd';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import s from './Modal.module.scss';
|
||||
import React, { FC, ReactNode, useState } from 'react';
|
||||
import styles from './Modal.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type ModalProps = {
|
||||
title: string;
|
||||
url?: string;
|
||||
visible: boolean;
|
||||
@@ -12,11 +12,19 @@ interface Props {
|
||||
children?: ReactNode;
|
||||
height?: string;
|
||||
width?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function Modal(props: Props) {
|
||||
const { title, url, visible, handleOk, handleCancel, afterClose, height, width, children } =
|
||||
props;
|
||||
export const Modal: FC<ModalProps> = ({
|
||||
title,
|
||||
url,
|
||||
visible,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
afterClose,
|
||||
height,
|
||||
width,
|
||||
children,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(!!url);
|
||||
|
||||
const modalStyle = {
|
||||
@@ -60,12 +68,13 @@ export default function Modal(props: Props) {
|
||||
)}
|
||||
|
||||
{iframe && <div style={{ display: iframeDisplayStyle }}>{iframe}</div>}
|
||||
{children && <div className={s.content}>{children}</div>}
|
||||
{loading && <Spin className={s.spinner} spinning={loading} size="large" />}
|
||||
{children && <div className={styles.content}>{children}</div>}
|
||||
{loading && <Spin className={styles.spinner} spinning={loading} size="large" />}
|
||||
</>
|
||||
</AntModal>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default Modal;
|
||||
|
||||
Modal.defaultProps = {
|
||||
url: undefined,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-alert */
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import NotifyReminderPopup from './NotifyReminderPopup';
|
||||
import { NotifyReminderPopup } from './NotifyReminderPopup';
|
||||
import Mock from '../../../stories/assets/mocks/notify-popup.png';
|
||||
|
||||
const Example = args => (
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Popover } from 'antd';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import s from './NotifyReminderPopup.module.scss';
|
||||
import React, { useState, useEffect, FC } from 'react';
|
||||
import styles from './NotifyReminderPopup.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type NotifyReminderPopupProps = {
|
||||
visible: boolean;
|
||||
children: React.ReactNode;
|
||||
notificationClicked: () => void;
|
||||
notificationClosed: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
export default function NotifyReminderPopup(props: Props) {
|
||||
const { children, visible, notificationClicked, notificationClosed } = props;
|
||||
export const NotifyReminderPopup: FC<NotifyReminderPopupProps> = ({
|
||||
children,
|
||||
visible,
|
||||
notificationClicked,
|
||||
notificationClosed,
|
||||
}) => {
|
||||
const [visiblePopup, setVisiblePopup] = useState(visible);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
@@ -23,7 +27,7 @@ export default function NotifyReminderPopup(props: Props) {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const title = <div className={s.title}>Stay updated!</div>;
|
||||
const title = <div className={styles.title}>Stay updated!</div>;
|
||||
const popupStyle = {
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
@@ -45,10 +49,10 @@ export default function NotifyReminderPopup(props: Props) {
|
||||
|
||||
const content = (
|
||||
<div onClick={popupClicked} onKeyDown={popupClicked} role="menuitem" tabIndex={0}>
|
||||
<button type="button" className={s.closebutton} onClick={popupClosed}>
|
||||
<button type="button" className={styles.closebutton} onClick={popupClosed}>
|
||||
<CloseOutlined />
|
||||
</button>
|
||||
<div className={s.contentbutton}>
|
||||
<div className={styles.contentbutton}>
|
||||
Click and never miss
|
||||
<br />
|
||||
future streams!
|
||||
@@ -71,4 +75,4 @@ export default function NotifyReminderPopup(props: Props) {
|
||||
</Popover>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import OfflineBanner from './OfflineBanner';
|
||||
import { OfflineBanner } from './OfflineBanner';
|
||||
import OfflineState from '../../../stories/assets/mocks/offline-state.png';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
import { Divider, Button } from 'antd';
|
||||
import { NotificationFilled } from '@ant-design/icons';
|
||||
import { FC } from 'react';
|
||||
import styles from './OfflineBanner.module.scss';
|
||||
|
||||
import s from './OfflineBanner.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type OfflineBannerProps = {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function OfflineBanner({ name, text }: Props) {
|
||||
const handleShowNotificationModal = () => {
|
||||
console.log('show notification modal');
|
||||
};
|
||||
export const OfflineBanner: FC<OfflineBannerProps> = ({ name, text }) => (
|
||||
<div className={styles.outerContainer}>
|
||||
<div className={styles.innerContainer}>
|
||||
<div className={styles.header}>{name} is currently offline.</div>
|
||||
<Divider />
|
||||
<div>{text}</div>
|
||||
|
||||
return (
|
||||
<div className={s.outerContainer}>
|
||||
<div className={s.innerContainer}>
|
||||
<div className={s.header}>{name} is currently offline.</div>
|
||||
<Divider />
|
||||
<div>{text}</div>
|
||||
|
||||
<div className={s.footer}>
|
||||
<Button type="primary" onClick={handleShowNotificationModal}>
|
||||
<NotificationFilled />
|
||||
Notify when Live
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<Button type="primary" onClick={() => console.log('show notification modal')}>
|
||||
<NotificationFilled />
|
||||
Notify when Live
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Sider from 'antd/lib/layout/Sider';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FC } from 'react';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import { ChatContainer } from '../../chat';
|
||||
import s from './Sidebar.module.scss';
|
||||
import { ChatContainer } from '../../chat/ChatContainer/ChatContainer';
|
||||
import styles from './Sidebar.module.scss';
|
||||
|
||||
import {
|
||||
chatDisplayNameAtom,
|
||||
@@ -11,14 +12,14 @@ import {
|
||||
visibleChatMessagesSelector,
|
||||
} from '../../stores/ClientConfigStore';
|
||||
|
||||
export default function Sidebar() {
|
||||
export const Sidebar: FC = () => {
|
||||
const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom);
|
||||
const chatUserId = useRecoilValue<string>(chatUserIdAtom);
|
||||
const isChatModerator = useRecoilValue<boolean>(isChatModeratorAtom);
|
||||
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
|
||||
|
||||
return (
|
||||
<Sider className={s.root} collapsedWidth={0} width={320}>
|
||||
<Sider className={styles.root} collapsedWidth={0} width={320}>
|
||||
<ChatContainer
|
||||
messages={messages}
|
||||
usernameToHighlight={chatDisplayName}
|
||||
@@ -27,4 +28,4 @@ export default function Sidebar() {
|
||||
/>
|
||||
</Sider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './Sidebar';
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import SocialLinks from './SocialLinks';
|
||||
import { SocialLinks } from './SocialLinks';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Components/Social links',
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { FC } from 'react';
|
||||
import { SocialLink } from '../../../interfaces/social-link.model';
|
||||
import s from './SocialLinks.module.scss';
|
||||
import styles from './SocialLinks.module.scss';
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
export type SocialLinksProps = {
|
||||
links: SocialLink[];
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function SocialLinks(props: Props) {
|
||||
const { links } = props;
|
||||
return (
|
||||
<div className={s.links}>
|
||||
{links.map(link => (
|
||||
<a key={link.platform} href={link.url} className={s.link} target="_blank" rel="noreferrer">
|
||||
<img src={link.icon} alt={link.platform} title={link.platform} className={s.link} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const SocialLinks: FC<SocialLinksProps> = ({ links }) => (
|
||||
<div className={styles.links}>
|
||||
{links.map(link => (
|
||||
<a
|
||||
key={link.platform}
|
||||
href={link.url}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img src={link.icon} alt={link.platform} title={link.platform} className={styles.link} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { subHours } from 'date-fns';
|
||||
import Statusbar from './Statusbar';
|
||||
import { Statusbar } from './Statusbar';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Player/Status bar',
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||
import intervalToDuration from 'date-fns/intervalToDuration';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
import s from './Statusbar.module.scss';
|
||||
import styles from './Statusbar.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type StatusbarProps = {
|
||||
online: Boolean;
|
||||
lastConnectTime?: Date;
|
||||
lastDisconnectTime?: Date;
|
||||
viewerCount: number;
|
||||
}
|
||||
};
|
||||
|
||||
function makeDurationString(lastConnectTime: Date): string {
|
||||
const diff = intervalToDuration({ start: lastConnectTime, end: new Date() });
|
||||
@@ -22,7 +22,13 @@ function makeDurationString(lastConnectTime: Date): string {
|
||||
|
||||
return `${diff.minutes} minutes ${diff.seconds} seconds`;
|
||||
}
|
||||
export default function Statusbar(props: Props) {
|
||||
|
||||
export const Statusbar: FC<StatusbarProps> = ({
|
||||
online,
|
||||
lastConnectTime,
|
||||
lastDisconnectTime,
|
||||
viewerCount,
|
||||
}) => {
|
||||
const [, setNow] = useState(new Date());
|
||||
|
||||
// Set a timer to update the status bar.
|
||||
@@ -33,8 +39,6 @@ export default function Statusbar(props: Props) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { online, lastConnectTime, lastDisconnectTime, viewerCount } = props;
|
||||
|
||||
let onlineMessage = '';
|
||||
let rightSideMessage: any;
|
||||
if (online && lastConnectTime) {
|
||||
@@ -53,12 +57,13 @@ export default function Statusbar(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.statusbar}>
|
||||
<div className={styles.statusbar}>
|
||||
<div>{onlineMessage}</div>
|
||||
<div>{rightSideMessage}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default Statusbar;
|
||||
|
||||
Statusbar.defaultProps = {
|
||||
lastConnectTime: null,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './Statusbar';
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import FollowerCollection from './FollowerCollection';
|
||||
import { FollowerCollection } from './FollowerCollection';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Components/Followers/Followers collection',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { Col, Pagination, Row } from 'antd';
|
||||
import { Follower } from '../../../../interfaces/follower';
|
||||
import SingleFollower from '../SingleFollower/SingleFollower';
|
||||
import s from '../SingleFollower/SingleFollower.module.scss';
|
||||
import { SingleFollower } from '../SingleFollower/SingleFollower';
|
||||
import styles from '../SingleFollower/SingleFollower.module.scss';
|
||||
|
||||
export default function FollowerCollection() {
|
||||
export const FollowerCollection: FC = () => {
|
||||
const ENDPOINT = '/api/followers';
|
||||
const ITEMS_PER_PAGE = 24;
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function FollowerCollection() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.followers}>
|
||||
<div className={styles.followers}>
|
||||
<Row wrap gutter={[10, 10]} justify="space-around">
|
||||
{followers.map(follower => (
|
||||
<Col>
|
||||
@@ -62,4 +62,4 @@ export default function FollowerCollection() {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import SingleFollower from './SingleFollower';
|
||||
import { SingleFollower } from './SingleFollower';
|
||||
import SingleFollowerMock from '../../../../stories/assets/mocks/single-follower.png';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import { Avatar, Col, Row } from 'antd';
|
||||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { Follower } from '../../../../interfaces/follower';
|
||||
import s from './SingleFollower.module.scss';
|
||||
import styles from './SingleFollower.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type SingleFollowerProps = {
|
||||
follower: Follower;
|
||||
}
|
||||
};
|
||||
|
||||
export default function SingleFollower(props: Props) {
|
||||
const { follower } = props;
|
||||
|
||||
return (
|
||||
<div className={s.follower}>
|
||||
<a href={follower.link} target="_blank" rel="noreferrer">
|
||||
<Row wrap={false}>
|
||||
<Col span={6}>
|
||||
<Avatar src={follower.image} alt="Avatar" className={s.avatar}>
|
||||
<img src="/logo" alt="Logo" className={s.placeholder} />
|
||||
</Avatar>
|
||||
</Col>
|
||||
<Col>
|
||||
<Row>{follower.name}</Row>
|
||||
<Row className={s.account}>{follower.username}</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const SingleFollower: FC<SingleFollowerProps> = ({ follower }) => (
|
||||
<div className={styles.follower}>
|
||||
<a href={follower.link} target="_blank" rel="noreferrer">
|
||||
<Row wrap={false}>
|
||||
<Col span={6}>
|
||||
<Avatar src={follower.image} alt="Avatar" className={styles.avatar}>
|
||||
<img src="/logo" alt="Logo" className={styles.placeholder} />
|
||||
</Avatar>
|
||||
</Col>
|
||||
<Col>
|
||||
<Row>{follower.name}</Row>
|
||||
<Row className={styles.account}>{follower.username}</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
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 ModIcon } from './ModIcon';
|
||||
export { default as ServerLogo } from './Logo';
|
||||
export { default as StatusBar } from './Statusbar';
|
||||
Reference in New Issue
Block a user