19 changed files with 191 additions and 103 deletions
@ -0,0 +1,7 @@ |
|||||||
|
.button { |
||||||
|
margin: 3px; |
||||||
|
.icon { |
||||||
|
width: 20px; |
||||||
|
margin-right: 3px; |
||||||
|
} |
||||||
|
} |
||||||
@ -1,17 +1,18 @@ |
|||||||
import { Button } from 'antd'; |
import { Button } from 'antd'; |
||||||
import { ExternalAction } from '../interfaces/external-action.interface'; |
import { ExternalAction } from '../interfaces/external-action.interface'; |
||||||
|
import s from './ActionButton.module.scss'; |
||||||
|
|
||||||
interface Props { |
interface Props { |
||||||
action: ExternalAction; |
action: ExternalAction; |
||||||
} |
} |
||||||
|
|
||||||
export default function ExternalActionButton(props: Props) { |
export default function ActionButton(props: Props) { |
||||||
const { action } = props; |
const { action } = props; |
||||||
const { url, title, description, icon, color, openExternally } = action; |
const { url, title, description, icon, color, openExternally } = action; |
||||||
|
|
||||||
return ( |
return ( |
||||||
<Button type="primary" style={{ backgroundColor: color }}> |
<Button type="primary" className={`${s.button}`} style={{ backgroundColor: color }}> |
||||||
<img src={icon} width="30px" alt={description} /> |
<img src={icon} className={`${s.icon}`} alt={description} /> |
||||||
{title} |
{title} |
||||||
</Button> |
</Button> |
||||||
); |
); |
||||||
@ -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>; |
||||||
|
} |
||||||
@ -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> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -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; |
||||||
|
} |
||||||
@ -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 { |
.player { |
||||||
height: 90vh; |
height: 80vh; |
||||||
width: 100%; |
width: 100%; |
||||||
background-color: green; |
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>; |
|
||||||
} |
|
||||||
@ -1,17 +1,15 @@ |
|||||||
import React from 'react'; |
import React from 'react'; |
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react'; |
import { ComponentStory, ComponentMeta } from '@storybook/react'; |
||||||
import ExternalActionButton from '../components/action-buttons/ExternalActionButton'; |
import ActionButton from '../components/action-buttons/ActionButton'; |
||||||
|
|
||||||
export default { |
export default { |
||||||
title: 'owncast/External action button', |
title: 'owncast/External action button', |
||||||
component: ExternalActionButton, |
component: ActionButton, |
||||||
parameters: {}, |
parameters: {}, |
||||||
} as ComponentMeta<typeof ExternalActionButton>; |
} as ComponentMeta<typeof ActionButton>; |
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const Template: ComponentStory<typeof ExternalActionButton> = args => ( |
const Template: ComponentStory<typeof ActionButton> = args => <ActionButton {...args} />; |
||||||
<ExternalActionButton {...args} /> |
|
||||||
); |
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const Example1 = Template.bind({}); |
export const Example1 = Template.bind({}); |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { ComponentStory, ComponentMeta } from '@storybook/react'; |
||||||
|
import ActionButtonRow from '../components/action-buttons/ActionButtonRow'; |
||||||
|
import ActionButton from '../components/action-buttons/ActionButton'; |
||||||
|
|
||||||
|
export default { |
||||||
|
title: 'owncast/External action button row', |
||||||
|
component: ActionButtonRow, |
||||||
|
parameters: {}, |
||||||
|
} as ComponentMeta<typeof ActionButtonRow>; |
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const Template: ComponentStory<typeof ActionButtonRow> = args => ( |
||||||
|
<ActionButtonRow>{args.buttons}</ActionButtonRow> |
||||||
|
); |
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const actions = [ |
||||||
|
{ |
||||||
|
url: 'https://owncast.online/docs', |
||||||
|
title: 'Documentation', |
||||||
|
description: 'Owncast Documentation', |
||||||
|
icon: 'https://owncast.online/images/logo.svg', |
||||||
|
color: '#5232c8', |
||||||
|
openExternally: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
url: 'https://opencollective.com/embed/owncast/donate', |
||||||
|
title: 'Support Owncast', |
||||||
|
description: 'Contribute to Owncast', |
||||||
|
icon: 'https://opencollective.com/static/images/opencollective-icon.svg', |
||||||
|
color: '#2b4863', |
||||||
|
openExternally: false, |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
const buttons = actions.map(action => <ActionButton action={action} />); |
||||||
|
export const Example1 = Template.bind({}); |
||||||
|
Example1.args = { |
||||||
|
buttons, |
||||||
|
}; |
||||||
@ -1,37 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react'; |
|
||||||
import ExternalActionButtonRow from '../components/action-buttons/ExternalActionButtonRow'; |
|
||||||
|
|
||||||
export default { |
|
||||||
title: 'owncast/External action button row', |
|
||||||
component: ExternalActionButtonRow, |
|
||||||
parameters: {}, |
|
||||||
} as ComponentMeta<typeof ExternalActionButtonRow>; |
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const Template: ComponentStory<typeof ExternalActionButtonRow> = args => ( |
|
||||||
<ExternalActionButtonRow {...args} /> |
|
||||||
); |
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
export const Example1 = Template.bind({}); |
|
||||||
Example1.args = { |
|
||||||
actions: [ |
|
||||||
{ |
|
||||||
url: 'https://owncast.online/docs', |
|
||||||
title: 'Documentation', |
|
||||||
description: 'Owncast Documentation', |
|
||||||
icon: 'https://owncast.online/images/logo.svg', |
|
||||||
color: '#5232c8', |
|
||||||
openExternally: false, |
|
||||||
}, |
|
||||||
{ |
|
||||||
url: 'https://opencollective.com/embed/owncast/donate', |
|
||||||
title: 'Support Owncast', |
|
||||||
description: 'Contribute to Owncast', |
|
||||||
icon: 'https://opencollective.com/static/images/opencollective-icon.svg', |
|
||||||
color: '#2b4863', |
|
||||||
openExternally: false, |
|
||||||
}, |
|
||||||
], |
|
||||||
}; |
|
||||||
@ -1,25 +1,25 @@ |
|||||||
import React from 'react'; |
import React from 'react'; |
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react'; |
import { ComponentStory, ComponentMeta } from '@storybook/react'; |
||||||
import StatusBar from '../components/video/StatusBar'; |
import { subHours } from 'date-fns'; |
||||||
|
import Statusbar from '../components/ui/Statusbar/Statusbar'; |
||||||
|
|
||||||
export default { |
export default { |
||||||
title: 'owncast/Status bar', |
title: 'owncast/Status bar', |
||||||
component: StatusBar, |
component: Statusbar, |
||||||
parameters: {}, |
parameters: {}, |
||||||
} as ComponentMeta<typeof StatusBar>; |
} as ComponentMeta<typeof Statusbar>; |
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const Template: ComponentStory<typeof Statusbar> = args => <Statusbar {...args} />; |
||||||
const Template: ComponentStory<typeof StatusBar> = args => <StatusBar {...args} />; |
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
export const Online = Template.bind({}); |
export const Online = Template.bind({}); |
||||||
Online.args = { |
Online.args = { |
||||||
online: true, |
online: true, |
||||||
viewers: 42, |
viewerCount: 42, |
||||||
timer: '10:42', |
lastConnectTime: subHours(new Date(), 3), |
||||||
}; |
}; |
||||||
|
|
||||||
export const Offline = Template.bind({}); |
export const Offline = Template.bind({}); |
||||||
Offline.args = { |
Offline.args = { |
||||||
online: false, |
online: false, |
||||||
|
lastDisconnectTime: subHours(new Date(), 3), |
||||||
}; |
}; |
||||||
|
|||||||
Loading…
Reference in new issue