Add action buttons and status bar
This commit is contained in:
@@ -2,7 +2,7 @@ import { Menu, Dropdown } from 'antd';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { ChatVisibilityState, ChatState } from '../interfaces/application-state';
|
||||
import { chatVisibilityAtom as chatVisibilityAtom } from './stores/ClientConfigStore';
|
||||
import { chatVisibilityAtom } from './stores/ClientConfigStore';
|
||||
|
||||
interface Props {
|
||||
username: string;
|
||||
|
||||
7
web/components/action-buttons/ActionButton.module.scss
Normal file
7
web/components/action-buttons/ActionButton.module.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.button {
|
||||
margin: 3px;
|
||||
.icon {
|
||||
width: 20px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import { Button } from 'antd';
|
||||
import { ExternalAction } from '../interfaces/external-action.interface';
|
||||
import s from './ActionButton.module.scss';
|
||||
|
||||
interface Props {
|
||||
action: ExternalAction;
|
||||
}
|
||||
|
||||
export default function ExternalActionButton(props: Props) {
|
||||
export default function ActionButton(props: Props) {
|
||||
const { action } = props;
|
||||
const { url, title, description, icon, color, openExternally } = action;
|
||||
|
||||
return (
|
||||
<Button type="primary" style={{ backgroundColor: color }}>
|
||||
<img src={icon} width="30px" alt={description} />
|
||||
<Button type="primary" className={`${s.button}`} style={{ backgroundColor: color }}>
|
||||
<img src={icon} className={`${s.icon}`} alt={description} />
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
12
web/components/action-buttons/ActionButtonRow.tsx
Normal file
12
web/components/action-buttons/ActionButtonRow.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import s from './ActionButtons.module.scss';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode[];
|
||||
}
|
||||
|
||||
export default function ActionButtonRow(props: Props) {
|
||||
const { children } = props;
|
||||
|
||||
return <div className={`${s.row}`}>{children}</div>;
|
||||
}
|
||||
10
web/components/action-buttons/ActionButtons.module.scss
Normal file
10
web/components/action-buttons/ActionButtons.module.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.row {
|
||||
margin: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
button {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { ExternalAction } from '../interfaces/external-action.interface';
|
||||
import ExternalActionButton from './ExternalActionButton';
|
||||
import s from './ExternalActionButtons.module.scss';
|
||||
|
||||
interface Props {
|
||||
actions: ExternalAction[];
|
||||
}
|
||||
|
||||
export default function ExternalActionButtonRow(props: Props) {
|
||||
const { actions } = props;
|
||||
|
||||
return (
|
||||
<div className={`${s.row}`}>
|
||||
{actions.map(action => (
|
||||
<ExternalActionButton key={action.id} action={action} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Layout, Row, Col, Tabs } from 'antd';
|
||||
import { Layout, Button, Col, Tabs } from 'antd';
|
||||
import Grid from 'antd/lib/card/Grid';
|
||||
import {
|
||||
chatVisibilityAtom,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
chatMessagesAtom,
|
||||
chatStateAtom,
|
||||
} from '../../stores/ClientConfigStore';
|
||||
import { serverStatusState } from '../../stores/ServerStatusStore';
|
||||
import { ClientConfig } from '../../../interfaces/client-config.model';
|
||||
import CustomPageContent from '../../CustomPageContent';
|
||||
import OwncastPlayer from '../../video/OwncastPlayer';
|
||||
@@ -18,40 +19,59 @@ import ChatContainer from '../../chat/ChatContainer';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import { ChatState, ChatVisibilityState } from '../../../interfaces/application-state';
|
||||
import ChatTextField from '../../chat/ChatTextField/ChatTextField';
|
||||
import ExternalActionButtonRow from '../../action-buttons/ExternalActionButtonRow';
|
||||
import ActionButtonRow from '../../action-buttons/ActionButtonRow';
|
||||
import ActionButton from '../../action-buttons/ActionButton';
|
||||
import Statusbar from '../Statusbar/Statusbar';
|
||||
import { ServerStatus } from '../../../interfaces/server-status.model';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
export default function FooterComponent() {
|
||||
const status = useRecoilValue<ServerStatus>(serverStatusState);
|
||||
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const chatOpen = useRecoilValue<ChatVisibilityState>(chatVisibilityAtom);
|
||||
const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
|
||||
const chatState = useRecoilValue<ChatState>(chatStateAtom);
|
||||
|
||||
const { extraPageContent } = clientConfig;
|
||||
const { online, viewerCount, lastConnectTime, lastDisconnectTime, streamTitle } = status;
|
||||
|
||||
const followers: Follower[] = [];
|
||||
|
||||
const total = 0;
|
||||
|
||||
// This is example content. It should be removed.
|
||||
const externalActions = [
|
||||
{
|
||||
url: 'https://owncast.online/docs',
|
||||
title: 'Example button',
|
||||
description: 'Example button description',
|
||||
icon: 'https://owncast.online/images/logo.svg',
|
||||
color: '#5232c8',
|
||||
openExternally: true,
|
||||
},
|
||||
];
|
||||
|
||||
const externalActionButtons = externalActions.map(action => (
|
||||
<ActionButton key={action.url} action={action} />
|
||||
));
|
||||
|
||||
return (
|
||||
<Content className={`${s.root}`} data-columns={chatOpen ? 2 : 1}>
|
||||
<div className={`${s.leftCol}`}>
|
||||
<OwncastPlayer source="https://watch.owncast.online" />
|
||||
<ExternalActionButtonRow
|
||||
actions={[
|
||||
{
|
||||
url: 'https://owncast.online/docs',
|
||||
title: 'Example button',
|
||||
description: 'Example button description',
|
||||
icon: 'https://owncast.online/images/logo.svg',
|
||||
color: '#5232c8',
|
||||
openExternally: true,
|
||||
},
|
||||
]}
|
||||
<Statusbar
|
||||
online={online}
|
||||
lastConnectTime={lastConnectTime}
|
||||
lastDisconnectTime={lastDisconnectTime}
|
||||
viewerCount={viewerCount}
|
||||
/>
|
||||
<ActionButtonRow>
|
||||
{externalActionButtons}
|
||||
<Button>Follow</Button>
|
||||
<Button>Notify</Button>
|
||||
</ActionButtonRow>
|
||||
<div className={`${s.lowerRow}`}>
|
||||
<Tabs defaultActiveKey="1" type="card">
|
||||
<TabPane tab="About" key="1" className={`${s.pageContentSection}`}>
|
||||
|
||||
10
web/components/ui/Statusbar/Statusbar.module.scss
Normal file
10
web/components/ui/Statusbar/Statusbar.module.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.statusbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
justify-content: space-between;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
background-color: black;
|
||||
}
|
||||
50
web/components/ui/Statusbar/Statusbar.tsx
Normal file
50
web/components/ui/Statusbar/Statusbar.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||
import formatDistanceToNowStrict from 'date-fns/formatDistanceToNowStrict';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import s from './Statusbar.module.scss';
|
||||
|
||||
interface Props {
|
||||
online: Boolean;
|
||||
lastConnectTime?: Date;
|
||||
lastDisconnectTime?: Date;
|
||||
viewerCount: number;
|
||||
}
|
||||
export default function Statusbar(props: Props) {
|
||||
const [now, setNow] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setNow(new Date()), 1000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { online, lastConnectTime, lastDisconnectTime, viewerCount } = props;
|
||||
|
||||
let onlineMessage = '';
|
||||
let rightSideMessage = '';
|
||||
if (online && lastConnectTime) {
|
||||
const diff = formatDistanceToNowStrict(new Date(lastConnectTime));
|
||||
onlineMessage = online ? `Streaming for ${diff}` : 'Offline';
|
||||
rightSideMessage = `${viewerCount} viewers`;
|
||||
} else {
|
||||
onlineMessage = 'Offline';
|
||||
if (lastDisconnectTime) {
|
||||
rightSideMessage = `Last live ${formatDistanceToNow(lastDisconnectTime)} ago.`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.statusbar}>
|
||||
{/* <div>{streamStartedAt}</div> */}
|
||||
<div>{onlineMessage}</div>
|
||||
<div>{rightSideMessage}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Statusbar.defaultProps = {
|
||||
lastConnectTime: null,
|
||||
lastDisconectTime: null,
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
.player {
|
||||
height: 90vh;
|
||||
height: 80vh;
|
||||
width: 100%;
|
||||
background-color: green;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
interface Props {
|
||||
online: boolean;
|
||||
viewers: number;
|
||||
timer: string;
|
||||
}
|
||||
|
||||
export default function StatusBar(props: Props) {
|
||||
return <div>Stream status bar goes here</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user