From f835ae5086bbae67aa518e28bf4486187b059e24 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Sat, 7 May 2022 16:13:06 -0700 Subject: [PATCH] Add action buttons and status bar --- web/components/UserDropdownMenu.tsx | 2 +- .../action-buttons/ActionButton.module.scss | 7 +++ ...ernalActionButton.tsx => ActionButton.tsx} | 7 +-- .../action-buttons/ActionButtonRow.tsx | 12 +++++ .../action-buttons/ActionButtons.module.scss | 10 ++++ .../ExternalActionButtonRow.tsx | 19 ------- .../ExternalActionButtons.module.scss | 0 web/components/ui/Content/Content.tsx | 48 ++++++++++++------ .../ui/Statusbar/Statusbar.module.scss | 10 ++++ web/components/ui/Statusbar/Statusbar.tsx | 50 +++++++++++++++++++ web/components/video/Player.module.scss | 2 +- web/components/video/StatusBar.jsx | 9 ---- ...n.stories.tsx => ActionButton.stories.tsx} | 10 ++-- web/stories/ActionButtonRow.stories.tsx | 41 +++++++++++++++ web/stories/Colors.stories.mdx | 2 +- .../ExternalActionButtonRow.stories.tsx | 37 -------------- web/stories/StatusBar.stories.tsx | 16 +++--- .../tokens/color/default-theme.yaml | 4 +- web/styles/variables.css | 8 ++- 19 files changed, 191 insertions(+), 103 deletions(-) create mode 100644 web/components/action-buttons/ActionButton.module.scss rename web/components/action-buttons/{ExternalActionButton.tsx => ActionButton.tsx} (54%) create mode 100644 web/components/action-buttons/ActionButtonRow.tsx create mode 100644 web/components/action-buttons/ActionButtons.module.scss delete mode 100644 web/components/action-buttons/ExternalActionButtonRow.tsx delete mode 100644 web/components/action-buttons/ExternalActionButtons.module.scss create mode 100644 web/components/ui/Statusbar/Statusbar.module.scss create mode 100644 web/components/ui/Statusbar/Statusbar.tsx delete mode 100644 web/components/video/StatusBar.jsx rename web/stories/{ExternalActionButton.stories.tsx => ActionButton.stories.tsx} (76%) create mode 100644 web/stories/ActionButtonRow.stories.tsx delete mode 100644 web/stories/ExternalActionButtonRow.stories.tsx diff --git a/web/components/UserDropdownMenu.tsx b/web/components/UserDropdownMenu.tsx index fdc1490e1..311e11958 100644 --- a/web/components/UserDropdownMenu.tsx +++ b/web/components/UserDropdownMenu.tsx @@ -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; diff --git a/web/components/action-buttons/ActionButton.module.scss b/web/components/action-buttons/ActionButton.module.scss new file mode 100644 index 000000000..f498d8750 --- /dev/null +++ b/web/components/action-buttons/ActionButton.module.scss @@ -0,0 +1,7 @@ +.button { + margin: 3px; + .icon { + width: 20px; + margin-right: 3px; + } +} \ No newline at end of file diff --git a/web/components/action-buttons/ExternalActionButton.tsx b/web/components/action-buttons/ActionButton.tsx similarity index 54% rename from web/components/action-buttons/ExternalActionButton.tsx rename to web/components/action-buttons/ActionButton.tsx index 5edb354dd..cd5a4496e 100644 --- a/web/components/action-buttons/ExternalActionButton.tsx +++ b/web/components/action-buttons/ActionButton.tsx @@ -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 ( - ); diff --git a/web/components/action-buttons/ActionButtonRow.tsx b/web/components/action-buttons/ActionButtonRow.tsx new file mode 100644 index 000000000..e4ae9b98f --- /dev/null +++ b/web/components/action-buttons/ActionButtonRow.tsx @@ -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
{children}
; +} diff --git a/web/components/action-buttons/ActionButtons.module.scss b/web/components/action-buttons/ActionButtons.module.scss new file mode 100644 index 000000000..709b35d27 --- /dev/null +++ b/web/components/action-buttons/ActionButtons.module.scss @@ -0,0 +1,10 @@ +.row { + margin: 5px; + display: flex; + align-items: center; + justify-content: flex-end; + button { + margin-left: 5px; + margin-right: 5px; + } +} diff --git a/web/components/action-buttons/ExternalActionButtonRow.tsx b/web/components/action-buttons/ExternalActionButtonRow.tsx deleted file mode 100644 index f3868b240..000000000 --- a/web/components/action-buttons/ExternalActionButtonRow.tsx +++ /dev/null @@ -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 ( -
- {actions.map(action => ( - - ))} -
- ); -} diff --git a/web/components/action-buttons/ExternalActionButtons.module.scss b/web/components/action-buttons/ExternalActionButtons.module.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/components/ui/Content/Content.tsx b/web/components/ui/Content/Content.tsx index f0b764714..15fd20b2d 100644 --- a/web/components/ui/Content/Content.tsx +++ b/web/components/ui/Content/Content.tsx @@ -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(serverStatusState); const clientConfig = useRecoilValue(clientConfigStateAtom); const chatOpen = useRecoilValue(chatVisibilityAtom); const messages = useRecoilValue(chatMessagesAtom); const chatState = useRecoilValue(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 => ( + + )); + return (
- + + {externalActionButtons} + + +
diff --git a/web/components/ui/Statusbar/Statusbar.module.scss b/web/components/ui/Statusbar/Statusbar.module.scss new file mode 100644 index 000000000..b4349e718 --- /dev/null +++ b/web/components/ui/Statusbar/Statusbar.module.scss @@ -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; +} \ No newline at end of file diff --git a/web/components/ui/Statusbar/Statusbar.tsx b/web/components/ui/Statusbar/Statusbar.tsx new file mode 100644 index 000000000..a14dbb4d8 --- /dev/null +++ b/web/components/ui/Statusbar/Statusbar.tsx @@ -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 ( +
+ {/*
{streamStartedAt}
*/} +
{onlineMessage}
+
{rightSideMessage}
+
+ ); +} + +Statusbar.defaultProps = { + lastConnectTime: null, + lastDisconectTime: null, +}; diff --git a/web/components/video/Player.module.scss b/web/components/video/Player.module.scss index 17466f7d5..66b2b8c75 100644 --- a/web/components/video/Player.module.scss +++ b/web/components/video/Player.module.scss @@ -1,5 +1,5 @@ .player { - height: 90vh; + height: 80vh; width: 100%; background-color: green; } \ No newline at end of file diff --git a/web/components/video/StatusBar.jsx b/web/components/video/StatusBar.jsx deleted file mode 100644 index 69304ff33..000000000 --- a/web/components/video/StatusBar.jsx +++ /dev/null @@ -1,9 +0,0 @@ -interface Props { - online: boolean; - viewers: number; - timer: string; -} - -export default function StatusBar(props: Props) { - return
Stream status bar goes here
; -} diff --git a/web/stories/ExternalActionButton.stories.tsx b/web/stories/ActionButton.stories.tsx similarity index 76% rename from web/stories/ExternalActionButton.stories.tsx rename to web/stories/ActionButton.stories.tsx index 2814ac87e..836ba3bd0 100644 --- a/web/stories/ExternalActionButton.stories.tsx +++ b/web/stories/ActionButton.stories.tsx @@ -1,17 +1,15 @@ import React from 'react'; import { ComponentStory, ComponentMeta } from '@storybook/react'; -import ExternalActionButton from '../components/action-buttons/ExternalActionButton'; +import ActionButton from '../components/action-buttons/ActionButton'; export default { title: 'owncast/External action button', - component: ExternalActionButton, + component: ActionButton, parameters: {}, -} as ComponentMeta; +} as ComponentMeta; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const Template: ComponentStory = args => ( - -); +const Template: ComponentStory = args => ; // eslint-disable-next-line @typescript-eslint/no-unused-vars export const Example1 = Template.bind({}); diff --git a/web/stories/ActionButtonRow.stories.tsx b/web/stories/ActionButtonRow.stories.tsx new file mode 100644 index 000000000..e618b080e --- /dev/null +++ b/web/stories/ActionButtonRow.stories.tsx @@ -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; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const Template: ComponentStory = args => ( + {args.buttons} +); + +// 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 => ); +export const Example1 = Template.bind({}); +Example1.args = { + buttons, +}; diff --git a/web/stories/Colors.stories.mdx b/web/stories/Colors.stories.mdx index e49bc6a21..e1fc9be5f 100644 --- a/web/stories/Colors.stories.mdx +++ b/web/stories/Colors.stories.mdx @@ -24,7 +24,7 @@ import {Color, ColorRow} from './Color'; ## Backgrounds - + ## Status diff --git a/web/stories/ExternalActionButtonRow.stories.tsx b/web/stories/ExternalActionButtonRow.stories.tsx deleted file mode 100644 index 961c15bc3..000000000 --- a/web/stories/ExternalActionButtonRow.stories.tsx +++ /dev/null @@ -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; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const Template: ComponentStory = 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, - }, - ], -}; diff --git a/web/stories/StatusBar.stories.tsx b/web/stories/StatusBar.stories.tsx index a1469a89d..6fa426444 100644 --- a/web/stories/StatusBar.stories.tsx +++ b/web/stories/StatusBar.stories.tsx @@ -1,25 +1,25 @@ import React from '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 { title: 'owncast/Status bar', - component: StatusBar, + component: Statusbar, parameters: {}, -} as ComponentMeta; +} as ComponentMeta; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const Template: ComponentStory = args => ; +const Template: ComponentStory = args => ; -// eslint-disable-next-line @typescript-eslint/no-unused-vars export const Online = Template.bind({}); Online.args = { online: true, - viewers: 42, - timer: '10:42', + viewerCount: 42, + lastConnectTime: subHours(new Date(), 3), }; export const Offline = Template.bind({}); Offline.args = { online: false, + lastDisconnectTime: subHours(new Date(), 3), }; diff --git a/web/style-definitions/tokens/color/default-theme.yaml b/web/style-definitions/tokens/color/default-theme.yaml index 6a7898bbc..f32768bf4 100644 --- a/web/style-definitions/tokens/color/default-theme.yaml +++ b/web/style-definitions/tokens/color/default-theme.yaml @@ -1,7 +1,7 @@ # These are the variables used in the user interface. # They can be overwritten to customize the look and feel. # We should write some documentation on how to do that when the time comes. -# The fewer there are the better as it'll be easier to customize and document. +# The fewer there are the easier it'll be easier to customize and document. theme: primary-color: @@ -24,7 +24,7 @@ theme: comment: "A secondary background color used in sections and controls." rounded-corners: value: "5px" - comment: "The radius of rounded corners." + comment: "The radius of rounded corners used in places." success-color: value: "{color.owncast.green.500.value}" diff --git a/web/styles/variables.css b/web/styles/variables.css index 6313feeb3..81936e003 100644 --- a/web/styles/variables.css +++ b/web/styles/variables.css @@ -22,7 +22,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. */ @@ -60,7 +62,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);