Revert "fix: split LiveShell into Astro layout + individual islands"

This reverts commit 6e1757410a.
This commit is contained in:
asepharyana
2026-07-02 03:54:44 +07:00
parent 91e14abbda
commit 24c738dd19
4 changed files with 174 additions and 131 deletions
+142
View File
@@ -0,0 +1,142 @@
// ─── LiveShell.tsx — Page shell for /live ────────────────────────────────────
// Composes Sidebar, Header, ParticleBackground, and the four live islands
// (VoiceControls, ActiveSpeakers, AudioVisualizer, NowPlaying) into a full-page
// responsive layout.
// ─────────────────────────────────────────────────────────────────────────────
import { useCallback, useEffect, useState } from "react";
import {
getGuilds,
getVoiceChannels,
getVoiceStatus,
} from "../shared/api/client.js";
import { Header } from "../shared/components/Header.js";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "../shared/components/index.js";
import { ParticleBackground } from "../shared/components/particles/ParticleBackground.js";
import { Sidebar } from "../shared/components/Sidebar.js";
import { useTheme } from "../shared/hooks/useTheme.js";
import type { Channel, Guild } from "../shared/types/guild.js";
import type { VoiceStatus } from "../shared/types/voice.js";
import { useVoiceStore } from "../stores/voice-store.js";
import ActiveSpeakers from "./ActiveSpeakers.js";
import AudioVisualizer from "./AudioVisualizer.js";
import NowPlaying from "./NowPlaying.js";
import VoiceControls from "./VoiceControls.js";
const emptyVoiceStatus: VoiceStatus = {
connected: false,
activeGuildId: null,
activeChannelId: null,
activeChannelName: null,
connections: [],
};
export default function LiveShell() {
const { mode, isDark, toggle: toggleTheme } = useTheme();
const [voiceStatus, setVoiceStatus] = useState<VoiceStatus>(emptyVoiceStatus);
const [guilds, setGuilds] = useState<Guild[]>([]);
const [voiceChannels, setVoiceChannels] = useState<Channel[]>([]);
// Watch store for guild changes so we can fetch voice channels
const guildId = useVoiceStore((state) => state.guildId);
// Fetch initial data on mount
useEffect(() => {
getVoiceStatus()
.then(setVoiceStatus)
.catch(() => {
/* offline — leave default */
});
getGuilds()
.then(setGuilds)
.catch(() => {
/* offline — leave default */
});
}, []);
// Refetch voice channels when guild selection changes
useEffect(() => {
if (guildId) {
getVoiceChannels(guildId)
.then(setVoiceChannels)
.catch(() => setVoiceChannels([]));
} else {
setVoiceChannels([]);
}
}, [guildId]);
const handleTabChange = useCallback((tab: string) => {
if (tab !== "live") {
window.location.href = "/";
}
}, []);
return (
<div className="relative min-h-screen bg-background text-foreground">
{/* Background layers */}
<ParticleBackground />
<div
className="fixed inset-0 pointer-events-none grid-pattern opacity-[0.03]"
aria-hidden="true"
/>
<div className="relative flex min-h-screen">
<Sidebar
activeTab="live"
onTabChange={handleTabChange}
collapsed={false}
recentMessages={[]}
/>
<main className="flex min-w-0 flex-1 flex-col">
<Header
activeTab="live"
wsStatus="disconnected"
voiceStatus={voiceStatus}
themeMode={mode}
isDark={isDark}
onThemeToggle={toggleTheme}
/>
<div className="flex-1 overflow-auto p-4 md:p-6 lg:p-8 pb-16 md:pb-0">
{/* Responsive grid: stacks on <1024px */}
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
{/* Main content column */}
<div className="space-y-6">
<VoiceControls guilds={guilds} voiceChannels={voiceChannels} />
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Live Audio</CardTitle>
</CardHeader>
<CardContent>
<AudioVisualizer barCount={48} height={32} />
</CardContent>
</Card>
<NowPlaying />
</div>
{/* Sidebar column */}
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Active Speakers</CardTitle>
</CardHeader>
<CardContent>
<ActiveSpeakers />
</CardContent>
</Card>
</div>
</div>
</div>
</main>
</div>
</div>
);
}
+13 -31
View File
@@ -1,47 +1,29 @@
// ─── VoiceControls.tsx — Voice bridge island ────────────────────────────────
// Self-contained: fetches guilds/channels internally, reads/writes useVoiceStore,
// calls API for connect/disconnect.
// Self-contained: reads/writes useVoiceStore, calls API for connect/disconnect.
// ─────────────────────────────────────────────────────────────────────────────
import { Radio } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import {
connectVoice,
disconnectVoice,
getGuilds,
getVoiceChannels,
} from "../shared/api/client.js";
import { useCallback, useState } from "react";
import { connectVoice, disconnectVoice } from "../shared/api/client.js";
import { Button, Select } from "../shared/components/index.js";
import type { Channel, Guild } from "../shared/types/guild.js";
import { useVoiceStore } from "../stores/voice-store.js";
export default function VoiceControls() {
interface VoiceControlsProps {
guilds: Guild[];
voiceChannels: Channel[];
}
export default function VoiceControls({
guilds,
voiceChannels,
}: VoiceControlsProps) {
const connected = useVoiceStore((state) => state.connected);
const guildId = useVoiceStore((state) => state.guildId);
const channelId = useVoiceStore((state) => state.channelId);
const setGuildChannel = useVoiceStore((state) => state.setGuildChannel);
const setConnected = useVoiceStore((state) => state.setConnected);
const [loading, setLoading] = useState(false);
const [guilds, setGuilds] = useState<Guild[]>([]);
const [voiceChannels, setVoiceChannels] = useState<Channel[]>([]);
// Fetch guilds on mount
useEffect(() => {
getGuilds()
.then(setGuilds)
.catch(() => { /* offline */ });
}, []);
// Fetch voice channels when guild selection changes
useEffect(() => {
if (guildId) {
getVoiceChannels(guildId)
.then(setVoiceChannels)
.catch(() => setVoiceChannels([]));
} else {
setVoiceChannels([]);
}
}, [guildId]);
const handleConnect = useCallback(async () => {
if (!guildId || !channelId) return;
@@ -69,7 +51,7 @@ export default function VoiceControls() {
}, [setConnected]);
return (
<div className="space-y-4">
<div className="glass rounded-xl p-6">
<h3 className="flex items-center gap-2 text-lg font-semibold tracking-tight">
<Radio className="h-5 w-5 text-primary" /> Voice Bridge
</h3>
+6 -100
View File
@@ -1,108 +1,14 @@
---
// ─── live.astro — Voice & Media live monitoring page ────────────────────────
// Layout shell (Sidebar, Header) rendered as zero-JS Astro components.
// Only the truly interactive parts are React islands with client directives.
// Full-page layout with Sidebar navigation, Header, and the LiveShell React
// island that composes all live components (VoiceControls, ActiveSpeakers,
// AudioVisualizer, NowPlaying).
// Particles and MascotChat are embedded inside the LiveShell island.
// ────────────────────────────────────────────────────────────────────────────
import BaseLayout from "../layouts/BaseLayout.astro";
import Sidebar from "../components/sidebar/Sidebar.astro";
import Header from "../components/header/Header.astro";
import Card from "../components/ui/Card.astro";
// React islands — interactive only
import ThemeToggle from "../islands/ThemeToggle";
import VoiceControls from "../islands/VoiceControls";
import ActiveSpeakers from "../islands/ActiveSpeakers";
import AudioVisualizer from "../islands/AudioVisualizer";
import NowPlaying from "../islands/NowPlaying";
import Particles from "../islands/Particles";
import MascotChat from "../islands/MascotChat";
import LiveShell from "../islands/LiveShell";
---
<BaseLayout>
<div class="live-page-layout">
<!-- Sidebar — zero JS Astro component -->
<Sidebar activeTab="live" />
<main class="live-main-area">
<!-- Header — zero JS, ThemeToggle is a tiny React island -->
<Header title="Live">
<ThemeToggle slot="actions" client:idle />
</Header>
<div class="live-content p-4 md:p-6 lg:p-8">
<!-- Responsive grid: stacks on <1024px -->
<div class="live-grid">
<!-- Main column -->
<div class="live-main-col space-y-4 md:space-y-6">
<!-- VoiceControls — core functionality, load immediately -->
<Card variant="glass">
<h2 class="text-base font-semibold mb-3">Voice Connection</h2>
<VoiceControls client:load />
</Card>
<!-- AudioVisualizer — deferred, not critical -->
<Card>
<h2 class="text-base font-semibold mb-3">Live Audio</h2>
<AudioVisualizer client:idle barCount={48} height={32} />
</Card>
<!-- NowPlaying — deferred -->
<Card>
<h2 class="text-base font-semibold mb-3">Now Playing</h2>
<NowPlaying client:idle />
</Card>
</div>
<!-- Sidebar column -->
<aside class="live-sidebar-col space-y-4 md:space-y-6">
<Card>
<h2 class="text-base font-semibold mb-3">Active Speakers</h2>
<ActiveSpeakers client:idle />
</Card>
</aside>
</div>
</div>
</main>
</div>
<!-- Deferred background & mascot -->
<Particles client:idle />
<MascotChat client:idle />
<LiveShell client:only="react" />
</BaseLayout>
<style is:global>
.live-page-layout {
display: grid;
grid-template-columns: auto 1fr;
min-height: 100vh;
}
.live-main-area {
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.live-content {
flex: 1;
overflow-y: auto;
}
.live-grid {
display: grid;
gap: 1.5rem;
}
@media (min-width: 1024px) {
.live-grid {
grid-template-columns: 1fr 320px;
}
}
.live-sidebar-col {
position: sticky;
top: calc(56px + 2rem);
align-self: start;
}
</style>