From e4a15b9c295c320e2e1ff099a7a9a0430d0f150f Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 24 Dec 2025 19:19:25 -0800 Subject: [PATCH] Add LiveNowSidebar component for live status notification and integrate it into relevant pages --- .../web/src/components/LiveNowSidebar.tsx | 98 +++++++++++++++++++ packages/web/src/routes/$username.tsx | 54 ++++------ packages/web/src/routes/index.tsx | 57 +---------- 3 files changed, 118 insertions(+), 91 deletions(-) create mode 100644 packages/web/src/components/LiveNowSidebar.tsx diff --git a/packages/web/src/components/LiveNowSidebar.tsx b/packages/web/src/components/LiveNowSidebar.tsx new file mode 100644 index 00000000..9ec2afe2 --- /dev/null +++ b/packages/web/src/components/LiveNowSidebar.tsx @@ -0,0 +1,98 @@ +import { useState, useEffect } from "react" +import { Link } from "@tanstack/react-router" +import { Radio, X } from "lucide-react" + +const DISMISS_KEY = "linsa_live_dismissed" +const DISMISS_DURATION_MS = 30 * 60 * 1000 // 30 minutes + +interface LiveNowSidebarProps { + /** Don't show on the nikiv page itself */ + currentUsername?: string +} + +export function LiveNowSidebar({ currentUsername }: LiveNowSidebarProps) { + const [isLive, setIsLive] = useState(false) + const [isDismissed, setIsDismissed] = useState(true) // Start hidden to avoid flash + + // Check if dismissed + useEffect(() => { + const stored = localStorage.getItem(DISMISS_KEY) + if (stored) { + const dismissedAt = parseInt(stored, 10) + if (Date.now() - dismissedAt < DISMISS_DURATION_MS) { + setIsDismissed(true) + return + } + } + setIsDismissed(false) + }, []) + + // Check live status + useEffect(() => { + const checkLiveStatus = async () => { + try { + const response = await fetch("/api/check-hls") + if (response.ok) { + const data = await response.json() + setIsLive(Boolean(data.isLive)) + } + } catch { + // Ignore errors + } + } + + checkLiveStatus() + const interval = setInterval(checkLiveStatus, 30000) + return () => clearInterval(interval) + }, []) + + const handleDismiss = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + localStorage.setItem(DISMISS_KEY, Date.now().toString()) + setIsDismissed(true) + } + + // Don't show if not live, dismissed, or already on nikiv's page + if (!isLive || isDismissed || currentUsername === "nikiv") { + return null + } + + return ( +
+ + {/* Dismiss button */} + + +
+
+ + +
+ Live Now +
+
+ nikiv +
+

nikiv

+

Streaming now

+
+
+ +
+ ) +} diff --git a/packages/web/src/routes/$username.tsx b/packages/web/src/routes/$username.tsx index 603e29a4..e8abfbe3 100644 --- a/packages/web/src/routes/$username.tsx +++ b/packages/web/src/routes/$username.tsx @@ -4,6 +4,7 @@ import { getStreamByUsername, type StreamPageData } from "@/lib/stream/db" import { VideoPlayer } from "@/components/VideoPlayer" import { CloudflareStreamPlayer } from "@/components/CloudflareStreamPlayer" import { WebRTCPlayer } from "@/components/WebRTCPlayer" +import { LiveNowSidebar } from "@/components/LiveNowSidebar" import { resolveStreamPlayback } from "@/lib/stream/playback" import { JazzProvider } from "@/lib/jazz/provider" import { ViewerCount } from "@/components/ViewerCount" @@ -140,56 +141,34 @@ function StreamPage() { useEffect(() => { let isActive = true - const setReadySafe = (ready: boolean) => { - if (isActive) { - setStreamReady(ready) - } - } - const setDataSafe = (next: StreamPageData | null) => { - if (isActive) { - setData(next) - } - } - const setLoadingSafe = (next: boolean) => { - if (isActive) { - setLoading(next) - } - } - const setErrorSafe = (next: string | null) => { - if (isActive) { - setError(next) - } - } - const setWebRtcFailedSafe = (next: boolean) => { - if (isActive) { - setWebRtcFailed(next) - } - } - - setReadySafe(false) - setWebRtcFailedSafe(false) // Special handling for nikiv - hardcoded stream if (username === "nikiv") { - setDataSafe(NIKIV_DATA) - setLoadingSafe(false) - + setData(NIKIV_DATA) + setLoading(false) return () => { isActive = false } } const loadData = async () => { - setLoadingSafe(true) - setErrorSafe(null) + if (!isActive) return + setLoading(true) + setError(null) try { const result = await getStreamByUsername(username) - setDataSafe(result) + if (isActive) { + setData(result) + } } catch (err) { - setErrorSafe("Failed to load stream") - console.error(err) + if (isActive) { + setError("Failed to load stream") + console.error(err) + } } finally { - setLoadingSafe(false) + if (isActive) { + setLoading(false) + } } } loadData() @@ -537,6 +516,7 @@ function StreamPage() { return ( +
{/* Main content area */}
diff --git a/packages/web/src/routes/index.tsx b/packages/web/src/routes/index.tsx index 2366e3a9..03e64b84 100644 --- a/packages/web/src/routes/index.tsx +++ b/packages/web/src/routes/index.tsx @@ -1,72 +1,21 @@ import { useState, useEffect, type FormEvent } from "react" import { createFileRoute, Link } from "@tanstack/react-router" import { ShaderBackground } from "@/components/ShaderBackground" +import { LiveNowSidebar } from "@/components/LiveNowSidebar" import { authClient } from "@/lib/auth-client" import { useAccount } from "jazz-tools/react" import { ViewerAccount, type SavedUrl } from "@/lib/jazz/schema" import { JazzProvider } from "@/lib/jazz/provider" -import { Link2, Plus, Trash2, ExternalLink, Video, Settings, LogOut, Radio } from "lucide-react" +import { Link2, Plus, Trash2, ExternalLink, Video, Settings, LogOut } from "lucide-react" // Feature flag: only this email can access stream features const STREAM_ENABLED_EMAIL = "nikita@nikiv.dev" -function LiveNowSidebar({ isLive }: { isLive: boolean }) { - if (!isLive) return null - - return ( -
- -
-
- - -
- Live Now -
-
- nikiv -
-

nikiv

-

Streaming now

-
-
- -
- ) -} - function LandingPage() { - const [isLive, setIsLive] = useState(false) - - useEffect(() => { - const checkLiveStatus = async () => { - try { - const response = await fetch("/api/stream-status") - if (response.ok) { - const data = await response.json() - setIsLive(Boolean(data.isLive)) - } - } catch { - // Ignore errors, just don't show live indicator - } - } - - checkLiveStatus() - const interval = setInterval(checkLiveStatus, 30000) // Check every 30s - return () => clearInterval(interval) - }, []) - return (
- + {/* Hero Section */}