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:
-13
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user