add live now panel with time elapsed
All checks were successful
CI / Maven unit tests (push) Successful in 16s
CI / Build and push images (push) Has been skipped
CI / Deploy to server (push) Has been skipped

This commit is contained in:
2026-07-30 21:52:43 +01:00
parent 08cfbd4cbb
commit 0b1ea8e089
6 changed files with 115 additions and 9 deletions

View 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;

View File

@@ -63,7 +63,7 @@ const VideoCard = ({ id, title, duration, createdAt, onEdit, onDelete, className
return ( return (
<Link to={`${playerPath}/${id}`}> <Link to={`${playerPath}/${id}`}>
<div className={clsx("flex flex-col group cursor-pointer", className)}> <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 <img
src={thumbnailAvailable ? thumbnailUrl : fallbackThumbnail} src={thumbnailAvailable ? thumbnailUrl : fallbackThumbnail}
alt="Video Thumbnail" alt="Video Thumbnail"

View File

@@ -14,6 +14,7 @@
--color-accent: #A85A38; --color-accent: #A85A38;
--color-accent-hover: #94492A; --color-accent-hover: #94492A;
--color-live-card: #F0EBE1; --color-live-card: #F0EBE1;
--color-fields: #F4F0E8;
/* Colour Fonts */ /* Colour Fonts */
--color-text-primary: #2B2620; --color-text-primary: #2B2620;
@@ -22,6 +23,7 @@
--color-muted: #9A9084; --color-muted: #9A9084;
--color-hover: #E7E0D2; --color-hover: #E7E0D2;
--color-hairline: #F0EBE2; --color-hairline: #F0EBE2;
--color-live-red: #B5432E;
} }
:root { :root {
@@ -42,4 +44,5 @@ body {
@utility font-heading { @utility font-heading {
font-family: "Newsreader", serif; font-family: "Newsreader", serif;
font-weight: 400; font-weight: 400;
color: #2B2620;
} }

View File

@@ -2,6 +2,10 @@ import {useAuth} from "../utils/AuthContext.tsx";
import NoAccessPage from "../components/NoAccessPage.tsx"; import NoAccessPage from "../components/NoAccessPage.tsx";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import Card from "../components/Card.tsx"; 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"; 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)]; return options[Math.floor(Math.random() * options.length)];
} }
const Home = () => { function getTimeFromISOString(date: string | null): string {
const { user } = useAuth(); if (date == null) {
const now = new Date(); 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', { const formatted: string = new Intl.DateTimeFormat('en-IE', {
weekday: 'long', weekday: 'long',
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
}).format(now); }).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) { if (user == null) {
return (<NoAccessPage />) return (<NoAccessPage />)
} }
@@ -63,7 +117,7 @@ const Home = () => {
<div className={"flex flex-row justify-between items-start pb-10"}> <div className={"flex flex-row justify-between items-start pb-10"}>
<div> <div>
<p className={"font-data text-faint-mono text-sm tracking-widest"}>{formatted.toLocaleUpperCase()}</p> <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> </div>
<Link to={"/stream"} className={"bg-accent rounded-md text-hover text-sm p-2 hover:bg-accent-hover shadow-md"}> <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> </Link>
</div> </div>
<Card className={"bg-live-card pb-10"}> {streamStatus?.isStreaming &&
<p></p> <Card className={"bg-live-card p-8"}>
</Card> <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> <h2 className={"font-heading text-2xl"}>Recent VoDs</h2>
</div> </div>

View File

@@ -243,6 +243,23 @@ const StreamControl = () => {
<div className="px-8 py-10 max-w-5xl mx-auto flex flex-col gap-5"> <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> <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 */} {/* Hero status bar */}
<Card className={clsx( <Card className={clsx(
"p-6 flex items-center justify-between gap-6 border-l-4", "p-6 flex items-center justify-between gap-6 border-l-4",

View File

@@ -55,6 +55,7 @@ type JobResponse = {
type StreamStatus = { type StreamStatus = {
isStreaming: boolean, isStreaming: boolean,
id: number | null, id: number | null,
startDate: string | null
} }
type StreamHistoryItem = { type StreamHistoryItem = {