restyle the rest of the pages
All checks were successful
CI / Maven unit tests (push) Successful in 14s
CI / Build and push images (push) Has been skipped
CI / Deploy to server (push) Has been skipped

This commit is contained in:
2026-08-02 16:32:00 +01:00
parent 11809a7da8
commit eab3f8be20
46 changed files with 2022 additions and 857 deletions

View File

@@ -1,5 +1,5 @@
import {useAuth} from "../utils/AuthContext.tsx";
import NoAccessPage from "../components/NoAccessPage.tsx";
import Landing from "./Landing.tsx";
import {Link, useNavigate} from "react-router-dom";
import Card from "../components/Card.tsx";
import {useEffect, useState} from "react";
@@ -7,10 +7,11 @@ import IsLivePulse from "../components/IsLivePulse.tsx";
import type {Clip, Marker, StreamStatus, Vod} from "../utils/types.ts";
import {getCurrentStream} from "../utils/api/stream.ts";
import VideoCard from "../components/video/VideoCard.tsx";
import {getMarkers} from "../utils/api/markers.ts";
import {createMarker, getMarkers} from "../utils/api/markers.ts";
import clsx from "clsx";
import {getVods} from "../utils/api/vods.ts";
import {getClips} from "../utils/api/clips.ts";
import DarkButton from "../components/buttons/DarkButton.tsx";
import Button from "../components/buttons/Button.tsx";
type TimeOfDay = "morning" | "afternoon" | "evening" | "night";
@@ -55,7 +56,7 @@ function getGreetingSubtext(date: Date = new Date()): string {
function getTimeFromISOString(date: string | null): string {
if (date == null) {
throw new Error("API has returned no start date");
return "--:--";
}
const d = new Date(date);
@@ -66,6 +67,22 @@ function getTimeFromISOString(date: string | null): string {
}).format(d);
}
type SectionHeaderProps = { title: string; to: string; count: number };
const SectionHeader = ({ title, to, count }: SectionHeaderProps) => (
<div className={"flex flex-row items-baseline justify-between gap-4 pb-4"}>
<h2 className={"font-heading text-2xl"}>{title}</h2>
{count > 0 && (
<Link
to={to}
className={"font-body text-[13.5px] text-accent hover:text-accent-hover transition-colors duration-150"}
>
All {count} <span aria-hidden={true}>→</span>
</Link>
)}
</div>
);
const Home = () => {
const now = new Date();
const formatted: string = new Intl.DateTimeFormat('en-IE', {
@@ -80,6 +97,11 @@ const Home = () => {
const [ markers, setMarkers ] = useState<Marker[]>([])
const [ clips, setClips ] = useState<Clip[]>([]);
const [ vods, setVods ] = useState<Vod[]>([]);
const [ counts, setCounts ] = useState({ vods: 0, clips: 0 });
const [ markerBusy, setMarkerBusy ] = useState(false);
const [ markerNote, setMarkerNote ] = useState<string | null>(null);
const isStreaming = streamStatus?.isStreaming ?? false;
const [ greeting ] = useState<string>(getGreetingSubtext(new Date()));
const navigate = useNavigate();
@@ -87,17 +109,7 @@ const Home = () => {
// Fetch Markers & Stream information
useEffect(() => {
getCurrentStream()
.then(streamStatus => {
setStreamStatus(streamStatus)
// ---- FOR TESTING ONLY, REMOVE AT THE END -----
const stream: StreamStatus = {
isStreaming : true,
id : 49,
startDate : "2026-07-30T18:23:01.123456Z"
}
setStreamStatus(stream);
})
.then(setStreamStatus)
getMarkers()
.then(data => {
@@ -111,6 +123,7 @@ const Home = () => {
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.slice(0, 4);
setClips(topSix);
setCounts(prev => ({ ...prev, clips: data.length }));
})
// Get top 3 vods
@@ -120,9 +133,24 @@ const Home = () => {
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.slice(0, 3);
setVods(topSix);
setCounts(prev => ({ ...prev, vods: data.length }));
})
}, []);
const handleDropMarker = async () => {
if (markerBusy) return;
setMarkerBusy(true);
try {
await createMarker(`Marker at ${elapsed}`);
setMarkers(await getMarkers());
setMarkerNote(`dropped at ${elapsed}`);
} catch (err) {
setMarkerNote(err instanceof Error ? err.message : "Couldn't drop that marker");
} finally {
setMarkerBusy(false);
}
};
useEffect(() => {
if (streamStatus?.startDate == null) {
setElapsed('00:00:00');
@@ -145,7 +173,7 @@ const Home = () => {
}, [streamStatus?.startDate]);
if (user == null) {
return (<NoAccessPage />)
return (<Landing />)
}
return (
@@ -161,47 +189,89 @@ const Home = () => {
</Link>
</div>
{streamStatus?.isStreaming &&
<Card className={"bg-live-card p-8 mb-8"}>
<div className={"flex flex-row items-center gap-2"}>
<IsLivePulse />
<p className={"text-xs font-data text-live-red"}>LIVE NOW</p>
</div>
{/* Live status — always present, so the page has the same shape either way. */}
<Card className={clsx("p-8 mb-8", isStreaming ? "bg-live-card" : "bg-card")}>
<div className={"flex flex-row items-center gap-2"}>
{isStreaming
? <IsLivePulse />
: <span className={"h-2 w-2 rounded-full bg-inactive-strong"} />}
<p className={clsx(
"text-xs font-data tracking-[0.12em]",
isStreaming ? "text-live-red" : "text-faint-mono"
)}>
{isStreaming ? "LIVE NOW" : "OFF AIR"}
</p>
</div>
<div className={"p-1"}></div>
<div className={"p-1"}></div>
<h2 className={"font-heading text-3xl"}>
Session {String(streamStatus.id).padStart(5, '0')} {' '}
recording since {getTimeFromISOString(streamStatus.startDate)}.</h2>
{isStreaming ? (
<>
<h2 className={"font-heading text-3xl"}>
Session {String(streamStatus?.id).padStart(5, '0')} {' '}
recording since {getTimeFromISOString(streamStatus?.startDate ?? null)}.</h2>
<div className={"flex flex-row items-center gap-10 pt-4"}>
<div className={"flex flex-col"}>
<h2 className={"font-data text-xl"}>{elapsed}</h2>
<p className={"text-xs text-muted"}>elapsed</p>
<div className={"flex flex-row items-center gap-10 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 className={"flex flex-col"}>
<h2 className={"font-data text-xl"}>{markers.length}</h2>
<p className={"text-xs text-muted"}>markers dropped</p>
</div>
</div>
<div className={"flex flex-col"}>
<h2 className={"font-data text-xl"}>{markers.length}</h2>
<p className={"text-xs text-muted"}>markers dropped</p>
<div className={"mt-4 flex items-center gap-4"}>
<Button onClick={handleDropMarker} disabled={markerBusy}>+ Drop marker</Button>
{markerNote && <p className={"font-data text-[12px] text-muted"}>{markerNote}</p>}
</div>
</div>
</>
) : (
<>
<h2 className={"font-heading text-3xl"}>Nothing is streaming right now.</h2>
<p className={"pt-2 font-body text-[14.5px] leading-[1.65] text-text-body max-w-[62ch] text-pretty"}>
Point OBS at the ingest URL with your stream key and this block turns over to
elapsed time and markers.
</p>
<div className={"mt-4"}>
<DarkButton>+ Drop Marker</DarkButton>
</div>
</Card>
}
<div className={"flex flex-row items-center gap-10 pt-4"}>
<div className={"flex flex-col"}>
<h2 className={"font-data text-xl"}>{counts.vods}</h2>
<p className={"text-xs text-muted"}>vods saved</p>
</div>
<div className={"flex flex-col"}>
<h2 className={"font-data text-xl"}>{counts.clips}</h2>
<p className={"text-xs text-muted"}>clips cut</p>
</div>
</div>
<div className={"mt-4"}>
<Link to={"/stream"}>
<Button variant={"secondary"}>Open the stream panel</Button>
</Link>
</div>
</>
)}
</Card>
<div className={"flex flex-col"}>
<h2 className={"font-heading text-2xl"}>Recent VoDs</h2>
<SectionHeader title={"Recent VoDs"} to={"/my-vods"} count={counts.vods} />
<div className={"grid grid-cols-3 gap-4"}>
<div className={"grid grid-cols-3 gap-5.5"}>
{vods.map((vod) => (
<VideoCard
key={vod.id}
id={vod.id}
title={vod.title}
duration={vod.duration}
createdAt={vod.createdAt}
width={vod.width}
height={vod.height}
fps={vod.fps}
fileSize={vod.fileSize}
mediaApiPath="/api/v1/vods"
playerPath="/vod"
itemLabel="vod"
@@ -211,16 +281,21 @@ const Home = () => {
</div>
</div>
<div className={"flex flex-col mt-4"}>
<h2 className={"font-heading text-2xl"}>Recent Clips</h2>
<div className={"flex flex-col mt-9"}>
<SectionHeader title={"Recent Clips"} to={"/my-clips"} count={counts.clips} />
<div className={"grid grid-cols-4 gap-4"}>
<div className={"grid grid-cols-4 gap-4.5"}>
{clips.map((clip) => (
<VideoCard
key={clip.id}
id={clip.id}
title={clip.title}
duration={clip.duration}
createdAt={clip.createdAt}
width={clip.width}
height={clip.height}
fps={clip.fps}
fileSize={clip.fileSize}
mediaApiPath="/api/v1/clips"
playerPath="/clip"
itemLabel="clip"