Set title and convert all class component to functional #4

Merged
torjus merged 6 commits from feature/set-title into master 2023-12-05 02:22:23 +00:00
6 changed files with 127 additions and 104 deletions

View File

@ -60,7 +60,6 @@ aside {
.menu-list { .menu-list {
list-style: none; list-style: none;
background-color: brown;
margin-top: 0px; margin-top: 0px;
} }

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<title>stream.t-juice.club</title> <title></title>
<script src="./js/app.tsx" type="module"></script> <script src="./js/app.tsx" type="module"></script>
<link rel="stylesheet" href="./css/style.css" /> <link rel="stylesheet" href="./css/style.css" />
<link rel="stylesheet" href="./css/fonts.css" /> <link rel="stylesheet" href="./css/fonts.css" />

View File

@ -1,4 +1,8 @@
export interface SiteInfo {
siteName: string
}
export class MinistreamApiClient { export class MinistreamApiClient {
ENV: string ENV: string
@ -21,14 +25,22 @@ export class MinistreamApiClient {
const resp = await fetch( const resp = await fetch(
url, url,
); );
const res = resp.json() as unknown as string[]; data = await resp.json() as unknown as string[];
data = res;
} }
catch (e) { catch (e) {
throw e; throw e;
} }
return data; const sortedStreams = data.sort((a, b) => {
if (a > b) {
return -1
} else if (a < b) {
return 1
}
return 0
})
return sortedStreams;
} }
async postOffer(streamKey: string, offer_sdp: RTCSessionDescription): Promise<RTCSessionDescription> { async postOffer(streamKey: string, offer_sdp: RTCSessionDescription): Promise<RTCSessionDescription> {
@ -47,4 +59,19 @@ export class MinistreamApiClient {
return answer return answer
} }
async siteInfo(): Promise<SiteInfo> {
var url = "/api/siteinfo"
if (this.ENV !== "production") {
url = "http://localhost:8080/api/siteinfo"
}
const resp = await fetch(url,
{
method: "GET",
})
const data = resp.json()
return data as unknown as SiteInfo
}
} }

View File

@ -1,92 +1,91 @@
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import React from "react";
import { Menu } from "./menu"; import { Menu } from "./menu";
import { createContext, useState } from "react"; import { createContext, useEffect, useState } from "react";
import { MediaContainer } from "./media"; import { MediaContainer } from "./media";
import { MinistreamApiClient } from "./api"; import { MinistreamApiClient } from "./api";
import React from "react";
type ThemeContextType = "light" | "dark"; interface AppProps {
api: MinistreamApiClient
const ThemeContext = createContext<ThemeContextType>("light");
type AppState = {
streamList: string[]
selectedStream: string | undefined
}
type AppProps = {}
export class App extends React.Component<AppProps, AppState> {
apiClient: MinistreamApiClient
constructor(props: AppProps) {
super(props)
this.state = {
streamList: [],
selectedStream: undefined
}
this.apiClient = new MinistreamApiClient()
this.updateSelect = this.updateSelect.bind(this)
} }
async componentDidMount(): Promise<void> { const titleKey = "ministream.title"
await this.updateStreamList()
setInterval(() => { function setTitleFromLocalstorage() {
this.updateStreamList() var title = localStorage.getItem(titleKey)
}, 10000) if (title) {
setTitle(title)
}
} }
async updateStreamList() { function setTitle(title: string) {
const streams = await this.apiClient.listStreams() const el = document.querySelector('title')
const sortedStreams = streams.sort((a, b) => { if (el) {
if (a > b) { el.textContent = title
return -1
} else if (a < b) {
return 1
} }
return 0
})
if (sortedStreams.length != this.state.streamList.length) {
this.setStreamList(streams)
} }
if (!sortedStreams.every((_, idx) => { export function App({ api }: AppProps) {
return sortedStreams[idx] === this.state.streamList[idx] const [streamList, setStreamList] = useState<string[]>([])
const [selectedStream, setSelectedStream] = useState<string | null>(null)
const updateSelect = (item: string | null) => {
setSelectedStream(item)
}
const updateStreamList = () => {
api.listStreams().then((list) => {
setStreamList(list)
if (list.length != streamList.length) {
setStreamList(list)
}
if (!list.every((_, idx) => {
return list[idx] === streamList[idx]
})) { })) {
this.setStreamList(streams) setStreamList(list)
} }
}
setStreamList(streamList: string[]) {
console.log("stream list updated")
this.setState((state) => {
return { selectedStream: state.selectedStream, streamList: streamList }
}) })
} }
updateSelect(selected: string): void { const updateTitle = () => {
if (selected !== this.state.selectedStream) { api.siteInfo().then((info) => {
this.setState((state) => { if (info.siteName != document.title) {
return { streamList: state.streamList, selectedStream: selected } setTitle(info.siteName)
}) localStorage.setItem(titleKey, info.siteName)
} }
})
return
} }
render() { setInterval(() => {
updateStreamList()
}, 10000)
useEffect(() => {
updateStreamList()
}, [])
useEffect(() => {
updateTitle()
}, [])
return ( return (
<> <>
<Menu <Menu
items={this.state.streamList} items={streamList}
selectedItem={this.state.selectedStream} selectedItem={selectedStream}
selectCallback={this.updateSelect} selectCallback={updateSelect}
/> />
<MediaContainer selectedStream={this.state.selectedStream} api={this.apiClient} /> <MediaContainer selectedStream={selectedStream} api={api} />
</> </>
) )
} }
}
const rootElement = document.getElementById("app"); const rootElement = document.getElementById("app");
setTitleFromLocalstorage()
if (rootElement) { if (rootElement) {
const root = createRoot(rootElement); const root = createRoot(rootElement)
root.render(<App />) const api = new MinistreamApiClient()
root.render(<App api={api} />)
} }

View File

@ -1,9 +1,10 @@
import React, { ComponentProps, useRef, useEffect, useState } from "react" import { ComponentProps, useRef, useEffect, useState } from "react"
import { MinistreamApiClient } from "./api" import { MinistreamApiClient } from "./api"
import { resolve } from "path" import { resolve } from "path"
import React from "react"
type MediaContainerProps = { type MediaContainerProps = {
selectedStream: string | undefined selectedStream: string | null
api: MinistreamApiClient api: MinistreamApiClient
} }

View File

@ -1,45 +1,42 @@
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import React from "react"; import React from "react";
type StreamSelectCallback = (selected: string) => void type StreamSelectCallback = (selected: string | null) => void
type MenuProps = { interface MenuProps {
items: string[] items: string[]
selectedItem: string | undefined selectedItem: string | null
selectCallback: StreamSelectCallback selectCallback: StreamSelectCallback
} }
export class Menu extends React.Component<MenuProps> { export function Menu({ items, selectedItem, selectCallback }: MenuProps) {
constructor(props: MenuProps) { const title = document.title
super(props) const menuitems = () => {
}
render() {
return (
<div id="menu">
<aside>
<a className="menu-heading" href="#">stream.t-juice.club</a>
<ul className="menu-list">
{this.menuitems()}
</ul>
</aside>
</div>
)
}
private menuitems() {
return ( return (
<> <>
{this.props.items.map((value, idx) => { {items.map((value, idx) => {
if (this.props.selectedItem == value) { if (selectedItem == value) {
return <li key={idx} className="menu-item menu-selected"><a href={"#" + value} className="menu-link">{value}</a></li> return <li key={idx} className="menu-item menu-selected"><a href={"#" + value} className="menu-link">{value}</a></li>
} else { } else {
return <li key={idx} onClick={() => { return <li key={idx} onClick={() => {
this.props.selectCallback(value) selectCallback(value)
}} className="menu-item"><a href={"#" + value} className="menu-link">{value}</a></li> }} className="menu-item"><a href={"#" + value} className="menu-link">{value}</a></li>
} }
})} })}
</> </>
) )
} }
return (
<div id="menu">
<aside>
<a className="menu-heading" onClick={() => {
selectCallback(null)
}}href="#">{title}</a>
<ul className="menu-list">
{menuitems()}
</ul>
</aside>
</div>
)
} }