diff --git a/web/components/layouts/main.tsx b/web/components/layouts/main.tsx new file mode 100644 index 000000000..b52a07885 --- /dev/null +++ b/web/components/layouts/main.tsx @@ -0,0 +1,93 @@ +import { useRecoilValue } from 'recoil'; +import { Layout, Row, Col } from 'antd'; +import { useState } from 'react'; +import { ServerStatus } from '../../models/ServerStatus'; +import { ServerStatusStore, serverStatusState } from '../stores/ServerStatusStore'; +import { ClientConfigStore, clientConfigState } from '../stores/ClientConfigStore'; +import { ClientConfig } from '../../models/ClientConfig'; + +const { Header, Content, Footer, Sider } = Layout; + +function Main() { + const serverStatus = useRecoilValue(serverStatusState); + const clientConfig = useRecoilValue(clientConfigState); + + const { name, version, extraPageContent } = clientConfig; + const [chatCollapsed, setChatCollapsed] = useState(false); + + const toggleChatCollapsed = () => { + setChatCollapsed(!chatCollapsed); + }; + + return ( + <> + + + + + + +
+ {name} + +
+ +
+ + Video player goes here + + + + + + +
+
+
Footer: Owncast {version}
+
+
+ + ); + // return ( + //
+ + // + //
+ // {name} + // + //
+ // + // + // + // Video player goes here + // + // + // + // + // + // + + // + // chat + // + // + // + //
Footer: Owncast {version}
+ //
+ //
+ // ); +} + +export default Main; diff --git a/web/components/stores/ClientConfigStore.tsx b/web/components/stores/ClientConfigStore.tsx new file mode 100644 index 000000000..5de9ae8d3 --- /dev/null +++ b/web/components/stores/ClientConfigStore.tsx @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; +import { ReactElement } from 'react-markdown/lib/react-markdown'; +import { atom, useRecoilState } from 'recoil'; +import { makeEmptyClientConfig, ClientConfig } from '../../models/ClientConfig'; +import ClientConfigService from '../../services/ClientConfigService'; + +export const clientConfigState = atom({ + key: 'clientConfigState', + default: makeEmptyClientConfig(), +}); + +export function ClientConfigStore(): ReactElement { + const [, setClientConfig] = useRecoilState(clientConfigState); + + const updateClientConfig = async () => { + try { + const config = await ClientConfigService.getConfig(); + console.log(`ClientConfig: ${JSON.stringify(config)}`); + setClientConfig(config); + } catch (error) { + console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`); + } + }; + + useEffect(() => { + updateClientConfig(); + }, []); + + return null; +} diff --git a/web/components/stores/ServerStatusStore.tsx b/web/components/stores/ServerStatusStore.tsx new file mode 100644 index 000000000..e4b3df6d6 --- /dev/null +++ b/web/components/stores/ServerStatusStore.tsx @@ -0,0 +1,34 @@ +import { useEffect } from 'react'; +import { ReactElement } from 'react-markdown/lib/react-markdown'; +import { atom, useRecoilState } from 'recoil'; +import { ServerStatus, makeEmptyServerStatus } from '../../models/ServerStatus'; +import ServerStatusService from '../../services/StatusService'; + +export const serverStatusState = atom({ + key: 'serverStatusState', + default: makeEmptyServerStatus(), +}); + +export function ServerStatusStore(): ReactElement { + const [, setServerStatus] = useRecoilState(serverStatusState); + + const updateServerStatus = async () => { + try { + const status = await ServerStatusService.getStatus(); + setServerStatus(status); + return status; + } catch (error) { + console.error(`serverStatusState -> getStatus() ERROR: \n${error}`); + return null; + } + }; + + useEffect(() => { + setInterval(() => { + updateServerStatus(); + }, 5000); + updateServerStatus(); + }, []); + + return null; +} diff --git a/web/models/ClientConfig.ts b/web/models/ClientConfig.ts new file mode 100644 index 000000000..a3d7fb5e5 --- /dev/null +++ b/web/models/ClientConfig.ts @@ -0,0 +1,73 @@ +export interface ClientConfig { + name: string; + summary: string; + logo: string; + tags: string[]; + version: string; + nsfw: boolean; + extraPageContent: string; + socialHandles: SocialHandle[]; + chatDisabled: boolean; + externalActions: any[]; + customStyles: string; + maxSocketPayloadSize: number; + federation: Federation; + notifications: Notifications; + authentication: Authentication; +} + +interface Authentication { + indieAuthEnabled: boolean; +} + +interface Federation { + enabled: boolean; + account: string; + followerCount: number; +} + +interface Notifications { + browser: Browser; +} + +interface Browser { + enabled: boolean; + publicKey: string; +} + +interface SocialHandle { + platform: string; + url: string; + icon: string; +} + +export function makeEmptyClientConfig(): ClientConfig { + return { + name: '', + summary: '', + logo: '', + tags: [], + version: '', + nsfw: false, + extraPageContent: '', + socialHandles: [], + chatDisabled: false, + externalActions: [], + customStyles: '', + maxSocketPayloadSize: 0, + federation: { + enabled: false, + account: '', + followerCount: 0, + }, + notifications: { + browser: { + enabled: false, + publicKey: '', + }, + }, + authentication: { + indieAuthEnabled: false, + }, + }; +} diff --git a/web/models/ServerStatus.ts b/web/models/ServerStatus.ts new file mode 100644 index 000000000..c940b6fb6 --- /dev/null +++ b/web/models/ServerStatus.ts @@ -0,0 +1,15 @@ +export interface ServerStatus { + online: boolean; + viewerCount: number; + lastConnectTime?: Date; + lastDisconnectTime?: Date; + versionNumber?: string; + streamTitle?: string; +} + +export function makeEmptyServerStatus(): ServerStatus { + return { + online: false, + viewerCount: 0, + }; +} diff --git a/web/package-lock.json b/web/package-lock.json index 3eacdd996..abb67411d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -30,6 +30,7 @@ "react-linkify": "1.0.0-alpha", "react-markdown": "8.0.0", "react-markdown-editor-lite": "1.3.2", + "recoil": "^0.7.2", "ua-parser-js": "1.0.2" }, "devDependencies": { @@ -18130,6 +18131,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "node_modules/hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE=" + }, "node_modules/handlebars": { "version": "4.7.7", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", @@ -26788,6 +26794,25 @@ "node": ">= 0.10" } }, + "node_modules/recoil": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.2.tgz", + "integrity": "sha512-OT4pI7FOUHcIoRtjsL5Lqq+lFFzQfir4MIbUkqyJ3nqv3WfBP1pHepyurqTsK5gw+T+I2R8+uOD28yH+Lg5o4g==", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/refractor": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", @@ -45217,6 +45242,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE=" + }, "handlebars": { "version": "4.7.7", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", @@ -51767,6 +51797,14 @@ "resolve": "^1.1.6" } }, + "recoil": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.2.tgz", + "integrity": "sha512-OT4pI7FOUHcIoRtjsL5Lqq+lFFzQfir4MIbUkqyJ3nqv3WfBP1pHepyurqTsK5gw+T+I2R8+uOD28yH+Lg5o4g==", + "requires": { + "hamt_plus": "1.0.2" + } + }, "refractor": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", diff --git a/web/package.json b/web/package.json index 3f2c262b6..0a14eadae 100644 --- a/web/package.json +++ b/web/package.json @@ -33,6 +33,7 @@ "react-linkify": "1.0.0-alpha", "react-markdown": "8.0.0", "react-markdown-editor-lite": "1.3.2", + "recoil": "^0.7.2", "ua-parser-js": "1.0.2" }, "devDependencies": { diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 531382b4e..4cc24611a 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,8 +1,10 @@ +import { RecoilRoot } from 'recoil'; +import Main from '../components/layouts/main'; + export default function Home() { return ( -
- This is where v2 of the Owncast web UI will be built. Begin with the layout component - https://ant.design/components/layout/ and edit pages/index.tsx. -
+ +
+ ); } diff --git a/web/services/ClientConfigService.ts b/web/services/ClientConfigService.ts new file mode 100644 index 000000000..27f2d2753 --- /dev/null +++ b/web/services/ClientConfigService.ts @@ -0,0 +1,12 @@ +import { ClientConfig } from '../models/ClientConfig'; +const ENDPOINT = `http://localhost:8080/api/config`; + +class ClientConfigService { + public static async getConfig(): Promise { + const response = await fetch(ENDPOINT); + const status = await response.json(); + return status; + } +} + +export default ClientConfigService; diff --git a/web/services/StatusService.ts b/web/services/StatusService.ts new file mode 100644 index 000000000..296332f3b --- /dev/null +++ b/web/services/StatusService.ts @@ -0,0 +1,13 @@ +import ServerStatus from '../models/ServerStatus'; + +const ENDPOINT = `http://localhost:8080/api/status`; + +class ServerStatusService { + public static async getStatus(): Promise { + const response = await fetch(ENDPOINT); + const status = await response.json(); + return status; + } +} + +export default ServerStatusService;