add live now panel with time elapsed
This commit is contained in:
13
frontend/src/components/IsLivePulse.tsx
Normal file
13
frontend/src/components/IsLivePulse.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
const IsLivePulse = () => {
|
||||
return (
|
||||
<span className={"relative flex h-2 w-2"}>
|
||||
<span
|
||||
className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 bg-live-red"
|
||||
></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-live-red"
|
||||
></span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default IsLivePulse;
|
||||
@@ -63,7 +63,7 @@ const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className
|
||||
return (
|
||||
<Link to={`${playerPath}/${id}`}>
|
||||
<div className={clsx("flex flex-col group cursor-pointer", className)}>
|
||||
<div className="relative overflow-hidden rounded-lg">
|
||||
<div className="relative overflow-hidden rounded-lg hover:shadow-xl transition-transform duration-300">
|
||||
<img
|
||||
src={thumbnailAvailable ? thumbnailUrl : fallbackThumbnail}
|
||||
alt="Video Thumbnail"
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
--color-accent: #A85A38;
|
||||
--color-accent-hover: #94492A;
|
||||
--color-live-card: #F0EBE1;
|
||||
--color-fields: #F4F0E8;
|
||||
|
||||
/* Colour Fonts */
|
||||
--color-text-primary: #2B2620;
|
||||
@@ -22,6 +23,7 @@
|
||||
--color-muted: #9A9084;
|
||||
--color-hover: #E7E0D2;
|
||||
--color-hairline: #F0EBE2;
|
||||
--color-live-red: #B5432E;
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -42,4 +44,5 @@ body {
|
||||
@utility font-heading {
|
||||
font-family: "Newsreader", serif;
|
||||
font-weight: 400;
|
||||
color: #2B2620;
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import {useAuth} from "../utils/AuthContext.tsx";
|
||||
import NoAccessPage from "../components/NoAccessPage.tsx";
|
||||
import {Link} from "react-router-dom";
|
||||
import Card from "../components/Card.tsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import IsLivePulse from "../components/IsLivePulse.tsx";
|
||||
import type {StreamStatus} from "../utils/types.ts";
|
||||
import {getCurrentStream} from "../utils/api/stream.ts";
|
||||
|
||||
type TimeOfDay = "morning" | "afternoon" | "evening" | "night";
|
||||
|
||||
@@ -44,16 +48,66 @@ function getGreetingSubtext(date: Date = new Date()): string {
|
||||
return options[Math.floor(Math.random() * options.length)];
|
||||
}
|
||||
|
||||
const Home = () => {
|
||||
const { user } = useAuth();
|
||||
const now = new Date();
|
||||
function getTimeFromISOString(date: string | null): string {
|
||||
if (date == null) {
|
||||
throw new Error("API has returned no start date");
|
||||
}
|
||||
|
||||
const d = new Date(date);
|
||||
return new Intl.DateTimeFormat('en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}).format(d);
|
||||
}
|
||||
|
||||
const Home = () => {
|
||||
const now = new Date();
|
||||
const formatted: string = new Intl.DateTimeFormat('en-IE', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
}).format(now);
|
||||
|
||||
const { user } = useAuth();
|
||||
const [ streamStatus, setStreamStatus ] = useState<StreamStatus | null>(null)
|
||||
const [ greeting ] = useState<string>(getGreetingSubtext(new Date()));
|
||||
|
||||
const [elapsed, setElapsed] = useState('00:00:00');
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentStream()
|
||||
.then(streamStatus => {
|
||||
setStreamStatus(streamStatus)
|
||||
|
||||
// ---- FOR TESTING ONLY, REMOVE AT THE END -----
|
||||
streamStatus.isStreaming = true;
|
||||
streamStatus.id = 49;
|
||||
streamStatus.startDate = "2026-07-30T14:23:01.123456Z"
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (streamStatus?.startDate == null) {
|
||||
setElapsed('00:00:00');
|
||||
return;
|
||||
}
|
||||
|
||||
const startMs = new Date(streamStatus?.startDate).getTime();
|
||||
|
||||
const tick = () => {
|
||||
const diffSec = Math.floor((Date.now() - startMs) / 1000);
|
||||
const hh = String(Math.floor(diffSec / 3600)).padStart(2, '0');
|
||||
const mm = String(Math.floor((diffSec % 3600) / 60)).padStart(2, '0');
|
||||
const ss = String(diffSec % 60).padStart(2, '0');
|
||||
setElapsed(`${hh}:${mm}:${ss}`);
|
||||
};
|
||||
|
||||
tick();
|
||||
const interval = setInterval(tick, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [streamStatus?.startDate]);
|
||||
|
||||
if (user == null) {
|
||||
return (<NoAccessPage />)
|
||||
}
|
||||
@@ -63,7 +117,7 @@ const Home = () => {
|
||||
<div className={"flex flex-row justify-between items-start pb-10"}>
|
||||
<div>
|
||||
<p className={"font-data text-faint-mono text-sm tracking-widest"}>{formatted.toLocaleUpperCase()}</p>
|
||||
<h1 className={"font-heading font text-5xl text-text-strong tracking-[-0.015em]"}>{getGreetingSubtext(new Date())}</h1>
|
||||
<h1 className={"font-heading font text-5xl text-text-strong tracking-[-0.015em]"}>{greeting}</h1>
|
||||
</div>
|
||||
|
||||
<Link to={"/stream"} className={"bg-accent rounded-md text-hover text-sm p-2 hover:bg-accent-hover shadow-md"}>
|
||||
@@ -71,11 +125,29 @@ const Home = () => {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card className={"bg-live-card pb-10"}>
|
||||
<p></p>
|
||||
</Card>
|
||||
{streamStatus?.isStreaming &&
|
||||
<Card className={"bg-live-card p-8"}>
|
||||
<div className={"flex flex-row items-center gap-2"}>
|
||||
<IsLivePulse />
|
||||
<p className={"text-xs font-data text-live-red"}>LIVE NOW</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={"p-1"}></div>
|
||||
|
||||
<h2 className={"font-heading text-2xl"}>
|
||||
Session {String(streamStatus.id).padStart(5, '0')} {' '}
|
||||
recording since {getTimeFromISOString(streamStatus.startDate)}.</h2>
|
||||
|
||||
<div className={"flex flex-row items-center gap-2 pt-4"}>
|
||||
<div className={"flex flex-col"}>
|
||||
<h2 className={"font-data text-xl"}>{elapsed}</h2>
|
||||
<p className={"text-xs text-muted"}>elapsed</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
|
||||
<div className={"pt-10"}>
|
||||
<h2 className={"font-heading text-2xl"}>Recent VoDs</h2>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -243,6 +243,23 @@ const StreamControl = () => {
|
||||
<div className="px-8 py-10 max-w-5xl mx-auto flex flex-col gap-5">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Stream Control</h1>
|
||||
|
||||
{isStreaming &&
|
||||
<Card className={"bg-card pb-10"}>
|
||||
<div className={"p-6"}>
|
||||
<p className={"pb-3 font-heading text-text-strong text-xl"}>Server credentials</p>
|
||||
|
||||
<p className={"font-body text-muted text-sm p-1"}>Ingest URL</p>
|
||||
<input
|
||||
type={"text"}
|
||||
contentEditable={"false"}
|
||||
className={"bg-fields rounded-xl focus:outline-none w-full p-3 text-text-strong text-sm font-data"}
|
||||
readOnly={true}
|
||||
defaultValue={"rtmp://vodsystem.ddns.net:1935/live"}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
|
||||
{/* Hero status bar */}
|
||||
<Card className={clsx(
|
||||
"p-6 flex items-center justify-between gap-6 border-l-4",
|
||||
|
||||
@@ -55,6 +55,7 @@ type JobResponse = {
|
||||
type StreamStatus = {
|
||||
isStreaming: boolean,
|
||||
id: number | null,
|
||||
startDate: string | null
|
||||
}
|
||||
|
||||
type StreamHistoryItem = {
|
||||
|
||||
Reference in New Issue
Block a user