diff --git a/web/README.md b/web/README.md
index 324eeaf1b..debfe7fa4 100644
--- a/web/README.md
+++ b/web/README.md
@@ -18,6 +18,15 @@ Make sure you're running an instance of Owncast on localhost:8080, as your copy
```npm run dev```
+### Components and Styles
+
+You can start the [Storybook](https://storybook.js.org/) UI for exploring, testing, and developing components by running:
+
+```npm run storybook```
+
+This allows for components to be made available without the need of the server to be running and changes to be made in
+isolation.
+
### Update the project
You can add or edit a pages by modifying `pages/something.js`. The page auto-updates as you edit the file.
diff --git a/web/components/CustomPageContent.tsx b/web/components/CustomPageContent.tsx
index 5a0bfbfcb..dae759bb2 100644
--- a/web/components/CustomPageContent.tsx
+++ b/web/components/CustomPageContent.tsx
@@ -4,5 +4,5 @@ interface Props {
export default function CustomPageContent(props: Props) {
const { content } = props;
- return
{content}
;
+ return
;
}
diff --git a/web/components/Follower.tsx b/web/components/Follower.tsx
new file mode 100644
index 000000000..e7d395dd3
--- /dev/null
+++ b/web/components/Follower.tsx
@@ -0,0 +1,9 @@
+import { Follower } from '../interfaces/follower';
+
+interface Props {
+ follower: Follower;
+}
+
+export default function FollowerCollection(props: Props) {
+ return This is a single follower
;
+}
diff --git a/web/components/FollowersCollection.tsx b/web/components/FollowersCollection.tsx
new file mode 100644
index 000000000..aa2eb333e
--- /dev/null
+++ b/web/components/FollowersCollection.tsx
@@ -0,0 +1,9 @@
+import { Follower } from '../interfaces/follower';
+
+interface Props {
+ followers: Follower[];
+}
+
+export default function FollowerCollection(props: Props) {
+ return List of followers go here
;
+}
diff --git a/web/components/chat/ChatContainer.tsx b/web/components/chat/ChatContainer.tsx
index d793bee62..e6a484d70 100644
--- a/web/components/chat/ChatContainer.tsx
+++ b/web/components/chat/ChatContainer.tsx
@@ -5,5 +5,5 @@ interface Props {
}
export default function ChatContainer(props: Props) {
- return Component goes here
;
+ return Chat container goes here
;
}
diff --git a/web/components/stores/ClientConfigStore.tsx b/web/components/stores/ClientConfigStore.tsx
index d4b026edd..18fff4fb2 100644
--- a/web/components/stores/ClientConfigStore.tsx
+++ b/web/components/stores/ClientConfigStore.tsx
@@ -3,6 +3,7 @@ import { ReactElement } from 'react-markdown/lib/react-markdown';
import { atom, useRecoilState } from 'recoil';
import { makeEmptyClientConfig, ClientConfig } from '../../interfaces/client-config.model';
import ClientConfigService from '../../services/client-config-service';
+import { ChatMessage } from '../../interfaces/chat-message.model';
// The config that comes from the API.
export const clientConfigState = atom({
@@ -15,11 +16,16 @@ export const chatCurrentlyVisible = atom({
default: false,
});
-export const chatDislayName = atom({
+export const chatDisplayName = atom({
key: 'chatDisplayName',
default: '',
});
+export const chatMessages = atom({
+ key: 'chatMessages',
+ default: [] as ChatMessage[],
+});
+
export function ClientConfigStore(): ReactElement {
const [, setClientConfig] = useRecoilState(clientConfigState);
diff --git a/web/components/ui/Content/Content.tsx b/web/components/ui/Content/Content.tsx
index dacc70208..2e5467fd9 100644
--- a/web/components/ui/Content/Content.tsx
+++ b/web/components/ui/Content/Content.tsx
@@ -1,7 +1,12 @@
import { useRecoilValue } from 'recoil';
+import { Layout, Row, Col, Tabs } from 'antd';
import { clientConfigState } from '../../stores/ClientConfigStore';
import { ClientConfig } from '../../../interfaces/client-config.model';
-import { Layout, Row, Col } from 'antd';
+import CustomPageContent from '../../CustomPageContent';
+import OwncastPlayer from '../../video/OwncastPlayer';
+import FollowerCollection from '../../FollowersCollection';
+
+const { TabPane } = Tabs;
const { Content } = Layout;
@@ -13,11 +18,20 @@ export default function FooterComponent() {
- Video player goes here
+
+
+
-
+
+
+
+
+
+
+
+
diff --git a/web/components/ui/Footer/Footer.tsx b/web/components/ui/Footer/Footer.tsx
index 80eabfb01..39f464e15 100644
--- a/web/components/ui/Footer/Footer.tsx
+++ b/web/components/ui/Footer/Footer.tsx
@@ -1,13 +1,9 @@
-import { useRecoilValue } from 'recoil';
-import { clientConfigState } from '../../stores/ClientConfigStore';
-import { ClientConfig } from '../../../interfaces/client-config.model';
import { Layout } from 'antd';
const { Footer } = Layout;
-export default function FooterComponent() {
- const clientConfig = useRecoilValue(clientConfigState);
- const { version } = clientConfig;
+export default function FooterComponent(props) {
+ const { version } = props;
return Footer: Owncast {version} ;
}
diff --git a/web/components/ui/Header/Header.tsx b/web/components/ui/Header/Header.tsx
index 854926032..51b70c77d 100644
--- a/web/components/ui/Header/Header.tsx
+++ b/web/components/ui/Header/Header.tsx
@@ -1,31 +1,17 @@
-import s from './Header.module.scss';
import { Layout } from 'antd';
-import { ServerStatusStore, serverStatusState } from '../../stores/ServerStatusStore';
-import {
- ClientConfigStore,
- clientConfigState,
- chatCurrentlyVisible,
-} from '../../stores/ClientConfigStore';
-import { ClientConfig } from '../../../interfaces/client-config.model';
-import { useRecoilState, useRecoilValue } from 'recoil';
-import { useEffect } from 'react';
+import UserDropdown from '../../UserDropdownMenu';
+import s from './Header.module.scss';
const { Header } = Layout;
-export default function HeaderComponent() {
- const clientConfig = useRecoilValue(clientConfigState);
- const [chatOpen, setChatOpen] = useRecoilState(chatCurrentlyVisible);
-
- const { name } = clientConfig;
-
- useEffect(() => {
- console.log({ chatOpen });
- }, [chatOpen]);
+export default function HeaderComponent(props) {
+ const { name } = props;
return (
+ Logo goes here
{name}
- setChatOpen(!chatOpen)}>Toggle Chat
+
);
}
diff --git a/web/components/ui/Sidebar/Sidebar.tsx b/web/components/ui/Sidebar/Sidebar.tsx
index c4e4dac81..2071b6913 100644
--- a/web/components/ui/Sidebar/Sidebar.tsx
+++ b/web/components/ui/Sidebar/Sidebar.tsx
@@ -1,12 +1,15 @@
import Sider from 'antd/lib/layout/Sider';
import { useRecoilValue } from 'recoil';
-import { chatCurrentlyVisible } from '../../stores/ClientConfigStore';
+import { ChatMessage } from '../../../interfaces/chat-message.model';
+import ChatContainer from '../../chat/ChatContainer';
+import { chatMessages } from '../../stores/ClientConfigStore';
export default function Sidebar() {
- let chatOpen = useRecoilValue(chatCurrentlyVisible);
+ const messages = useRecoilValue(chatMessages);
+
return (
+ >
+
+
);
}
diff --git a/web/interfaces/external-action.tsx b/web/interfaces/external-action.ts
similarity index 100%
rename from web/interfaces/external-action.tsx
rename to web/interfaces/external-action.ts
diff --git a/web/interfaces/follower.ts b/web/interfaces/follower.ts
new file mode 100644
index 000000000..36be283fd
--- /dev/null
+++ b/web/interfaces/follower.ts
@@ -0,0 +1,7 @@
+export interface Follower {
+ name: string;
+ description?: string;
+ username?: string;
+ image?: string;
+ link: string;
+}
diff --git a/web/stories/Follower.stories.tsx b/web/stories/Follower.stories.tsx
new file mode 100644
index 000000000..2f2847dbd
--- /dev/null
+++ b/web/stories/Follower.stories.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import * as FollowerComponent from '../components/Follower';
+
+export default {
+ title: 'owncast/Follower',
+ component: FollowerComponent,
+ parameters: {},
+} as ComponentMeta;
+
+const Template: ComponentStory = args => ;
+
+export const Example = Template.bind({});
+Example.args = {
+ follower: {
+ name: 'John Doe',
+ description: 'User',
+ username: '@account@domain.tld',
+ image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
+ link: 'https://yahoo.com',
+ },
+};
diff --git a/web/stories/Followercollection.stories.tsx b/web/stories/Followercollection.stories.tsx
new file mode 100644
index 000000000..f1ea227cd
--- /dev/null
+++ b/web/stories/Followercollection.stories.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import FollowerCollection from '../components/FollowersCollection';
+
+export default {
+ title: 'owncast/Follower collection',
+ component: FollowerCollection,
+ parameters: {},
+} as ComponentMeta;
+
+const Template: ComponentStory = args => (
+
+);
+
+export const Example = Template.bind({});
+Example.args = {
+ followers: [
+ {
+ name: 'John Doe',
+ description: 'User',
+ username: '@account@domain.tld',
+ image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
+ link: 'https://yahoo.com',
+ },
+ {
+ name: 'John Doe',
+ description: 'User',
+ username: '@account@domain.tld',
+ image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
+ link: 'https://yahoo.com',
+ },
+ {
+ name: 'John Doe',
+ description: 'User',
+ username: '@account@domain.tld',
+ image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
+ link: 'https://yahoo.com',
+ },
+ {
+ name: 'John Doe',
+ description: 'User',
+ username: '@account@domain.tld',
+ image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
+ link: 'https://yahoo.com',
+ },
+ {
+ name: 'John Doe',
+ description: 'User',
+ username: '@account@domain.tld',
+ image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
+ link: 'https://yahoo.com',
+ },
+ {
+ name: 'John Doe',
+ description: 'User',
+ username: '@account@domain.tld',
+ image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
+ link: 'https://yahoo.com',
+ },
+ ],
+};
diff --git a/web/stories/Footer.stories.tsx b/web/stories/Footer.stories.tsx
new file mode 100644
index 000000000..fa3ad61d1
--- /dev/null
+++ b/web/stories/Footer.stories.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import Footer from '../components/ui/Footer/Footer';
+
+export default {
+ title: 'owncast/Footer',
+ component: Footer,
+ parameters: {},
+} as ComponentMeta;
+
+// eslint-disable-next-line @typescript-eslint/no-unused-Footer
+const Template: ComponentStory = args => ;
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const Example = Template.bind({});
+Example.args = {
+ version: 'v1.2.3',
+};
diff --git a/web/stories/Header.stories.tsx b/web/stories/Header.stories.tsx
new file mode 100644
index 000000000..ad4fa9acd
--- /dev/null
+++ b/web/stories/Header.stories.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import Header from '../components/ui/Header/Header';
+
+export default {
+ title: 'owncast/Header',
+ component: Header,
+ parameters: {},
+} as ComponentMeta;
+
+const Template: ComponentStory = args => ;
+
+export const Example = Template.bind({});
+Example.args = {
+ name: 'Example Stream Name',
+};