103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
import { useCallback, useMemo, useState } from "react";
|
||
import { useNavigate, useParams } from "react-router-dom";
|
||
import { CircleDot, CircleCheckBig } from "lucide-react";
|
||
import { particlePath } from "@/lib/particle-path";
|
||
import { ParticleListView } from "@/features/particles/particle-list-view";
|
||
import { VideoAudioToggle } from "@/components/video-audio-toggle";
|
||
import { ComposeOverlay } from "./compose/compose-overlay";
|
||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||
import { useStreamParticles } from "@/hooks/use-stream-particles";
|
||
import { useStreamKeyboardNav } from "@/hooks/use-stream-keyboard-nav";
|
||
|
||
/**
|
||
* Route-level component for /:networkId (index).
|
||
* Shows root-level particles for the selected network.
|
||
*/
|
||
export default function NetworkRoot() {
|
||
const { networkId } = useParams();
|
||
const navigate = useNavigate();
|
||
const path = particlePath(networkId!, []);
|
||
|
||
const { streams, isLoading } = useStreamParticles(path);
|
||
const [composeActive, setComposeActive] = useState(false);
|
||
const [statusTab, setStatusTab] = useState<"open" | "closed">("open");
|
||
|
||
const filteredStreams = useMemo(
|
||
() => streams.filter((s) => s.status === statusTab),
|
||
[streams, statusTab],
|
||
);
|
||
|
||
const { selectedIndex } = useStreamKeyboardNav({
|
||
streams: filteredStreams,
|
||
enabled: !composeActive,
|
||
onNavigate: useCallback(
|
||
(streamId: string) => navigate(`/${networkId}/${streamId}`),
|
||
[navigate, networkId],
|
||
),
|
||
});
|
||
|
||
return (
|
||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||
{/* Top bar — stays in place */}
|
||
<div className="flex shrink-0 items-center justify-between p-1 border-b">
|
||
<Tabs
|
||
value={statusTab}
|
||
onValueChange={(v) => setStatusTab(v as "open" | "closed")}
|
||
>
|
||
<TabsList>
|
||
<TabsTrigger value="open"><CircleDot className="size-3 text-green-500" /> Open</TabsTrigger>
|
||
<TabsTrigger value="closed"><CircleCheckBig className="size-3" /> Closed</TabsTrigger>
|
||
</TabsList>
|
||
</Tabs>
|
||
</div>
|
||
|
||
{/* Scrollable content */}
|
||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain pb-14 py-2">
|
||
<ParticleListView streams={filteredStreams} networkId={networkId!} isLoading={isLoading} selectedIndex={selectedIndex} />
|
||
</div>
|
||
|
||
<ComposeOverlay networkId={networkId!} onActiveChange={setComposeActive} />
|
||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 flex justify-center p-3">
|
||
<div className="pointer-events-auto">
|
||
<NetworkRootControls />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function NetworkRootControls() {
|
||
return (
|
||
<div className="flex items-center gap-4 text-sm text-white/50">
|
||
<span>
|
||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||
↑↓
|
||
</kbd>{" "}
|
||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||
Enter
|
||
</kbd>{" "}
|
||
navigate
|
||
</span>
|
||
<span>
|
||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||
1–9
|
||
</kbd>{" "}
|
||
jump
|
||
</span>
|
||
<VideoAudioToggle />
|
||
<span>
|
||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||
Hold `
|
||
</kbd>{" "}
|
||
to start
|
||
</span>
|
||
<span>
|
||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||
T
|
||
</kbd>{" "}
|
||
text
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|