fix: split LiveShell into Astro layout + individual islands

- Rewrite live.astro to use Astro Sidebar/Header/Card components
- VoiceControls becomes self-contained (fetches guilds/channels internally)
- Remove LiveShell.tsx (no longer needed)
- Each React island uses appropriate client:load/idle directive
This commit is contained in:
asepharyana
2026-07-02 03:13:54 +07:00
parent 134674d7dd
commit 6e1757410a
4 changed files with 131 additions and 174 deletions
-13
View File
@@ -1,13 +0,0 @@
[submodule "vendor/discord.js-selfbot-v13"]
path = vendor/discord.js-selfbot-v13
url = https://github.com/MythEclipse/discord.js-selfbot-v13.git
[submodule "vendor/discord-video-stream"]
path = vendor/discord-video-stream
url = https://github.com/MythEclipse/discord-video-stream.git
[submodule "vendor/drizzle-orm"]
path = vendor/drizzle-orm
url = https://github.com/MythEclipse/drizzle-orm.git
[submodule "vendor/better-sqlite3"]
path = vendor/better-sqlite3
url = https://github.com/MythEclipse/better-sqlite3.git
-142
View File
@@ -1,142 +0,0 @@
// ─── 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>
);
}
+31 -13
View File
@@ -1,29 +1,47 @@
// ─── VoiceControls.tsx — Voice bridge island ────────────────────────────────
// Self-contained: reads/writes useVoiceStore, calls API for connect/disconnect.
// Self-contained: fetches guilds/channels internally, reads/writes useVoiceStore,
// calls API for connect/disconnect.
// ─────────────────────────────────────────────────────────────────────────────
import { Radio } from "lucide-react";
import { useCallback, useState } from "react";
import { connectVoice, disconnectVoice } from "../shared/api/client.js";
import { useCallback, useEffect, useState } from "react";
import {
connectVoice,
disconnectVoice,
getGuilds,
getVoiceChannels,
} 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";
interface VoiceControlsProps {
guilds: Guild[];
voiceChannels: Channel[];
}
export default function VoiceControls({
guilds,
voiceChannels,
}: VoiceControlsProps) {
export default function VoiceControls() {
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;
@@ -51,7 +69,7 @@ export default function VoiceControls({
}, [setConnected]);
return (
<div className="glass rounded-xl p-6">
<div className="space-y-4">
<h3 className="flex items-center gap-2 text-lg font-semibold tracking-tight">
<Radio className="h-5 w-5 text-primary" /> Voice Bridge
</h3>
+100 -6
View File
@@ -1,14 +1,108 @@
---
// ─── live.astro — Voice & Media live monitoring page ────────────────────────
// 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.
// Layout shell (Sidebar, Header) rendered as zero-JS Astro components.
// Only the truly interactive parts are React islands with client directives.
// ────────────────────────────────────────────────────────────────────────────
import BaseLayout from "../layouts/BaseLayout.astro";
import LiveShell from "../islands/LiveShell";
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";
---
<BaseLayout>
<LiveShell client:only="react" />
<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 />
</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>