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:
James Young
2022-09-07 09:00:28 +02:00
committed by GitHub
parent ee333ef10a
commit d1f3fffe2f
178 changed files with 1258 additions and 1227 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
export { default } from './Footer';

View File

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

View File

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

View File

@@ -1 +0,0 @@
export { default } from './Header';

View File

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

View File

@@ -1 +0,0 @@
export { default } from './Logo';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
export { default } from './Sidebar';

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
export { default } from './Statusbar';

View File

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

View File

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

View File

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

View File

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

View File

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