feat: add app header, sidebar, and mobile navigation components
Deploy to VPS / deploy (push) Failing after 1m45s

- Implemented AppHeader component with theme toggle and connection status.
- Created AppSidebar component for navigation with connection status indicator.
- Added MobileNav component for mobile navigation with responsive design.
- Introduced shared components: DetailStat, EmptyState, ErrorState, LoadingSkeleton, and StatCard for consistent UI.
- Developed hooks for async data fetching: useAsync, useConfig, useDashboard, useGuilds, useMedia, useMessages, useRecordings, and useVoice.
- Added chatbot API functions for sending messages and managing chat history.
This commit is contained in:
asepharyana
2026-07-26 16:14:32 +07:00
parent 726ea8fca5
commit d5a547eb25
35 changed files with 2385 additions and 1733 deletions
@@ -8,81 +8,29 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Slider } from "@/components/ui/slider";
import { voiceApi } from "@/lib/api";
import type { MediaState } from "@/lib/types";
import { useMediaState } from "@/hooks";
import { useWebSocket } from "@/lib/ws/context";
export default function MediaPage() {
const ws = useWebSocket();
const [mediaState, setMediaState] = useState<MediaState | null>(null);
const { mediaState, refresh, queue, skip, stop, setVolume } = useMediaState();
const [queueUrl, setQueueUrl] = useState("");
const fetchMediaStatus = useCallback(async () => {
try {
const state = await voiceApi.getMediaStatus();
setMediaState(state);
} catch {
// ignore
}
}, []);
useEffect(() => {
fetchMediaStatus();
}, [fetchMediaStatus]);
refresh();
}, [refresh]);
// WS subscription
// WS subscription for real-time media state
useEffect(() => {
const unsubMedia = ws.on("media_state", (state) => {
setMediaState(state as MediaState);
});
const unsub = ws.on("media_state", () => refresh());
return unsub;
}, [ws, refresh]);
return () => {
unsubMedia();
};
}, [ws]);
const handleQueueMedia = useCallback(async () => {
const handleQueue = useCallback(() => {
if (!queueUrl.trim()) return;
try {
const state = await voiceApi.mediaQueue(queueUrl.trim(), "music");
setMediaState(state);
setQueueUrl("");
} catch {
// ignore
}
}, [queueUrl]);
const handleSkip = useCallback(async () => {
try {
const state = await voiceApi.mediaSkip();
setMediaState(state);
} catch {
// ignore
}
}, []);
const handleStop = useCallback(async () => {
try {
const state = await voiceApi.mediaStop();
setMediaState(state);
} catch {
// ignore
}
}, []);
const handleVolume = useCallback(
async (value: number | readonly number[]) => {
const vol = Array.isArray(value) ? value[0] : value;
try {
const state = await voiceApi.mediaVolume(vol);
setMediaState(state);
} catch {
// ignore
}
},
[],
);
queue(queueUrl.trim());
setQueueUrl("");
}, [queueUrl, queue]);
return (
<div className="space-y-5 animate-fade-in-up">
@@ -94,23 +42,20 @@ export default function MediaPage() {
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Queue URL */}
<div className="flex gap-2">
<Input
type="text"
placeholder="Queue a URL (YouTube, audio file…)"
value={queueUrl}
onChange={(e) => setQueueUrl(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleQueueMedia()}
onKeyDown={(e) => e.key === "Enter" && handleQueue()}
className="flex-1 h-9"
/>
<Button onClick={handleQueueMedia} disabled={!queueUrl.trim()}>
<Button onClick={handleQueue} disabled={!queueUrl.trim()}>
<Play className="size-4 mr-1.5" />
Queue
</Button>
</div>
{/* Now Playing */}
{mediaState?.current && (
<div className="rounded-lg bg-gradient-to-br from-primary/5 to-primary/[0.02] border border-primary/10 p-4 space-y-2">
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider flex items-center gap-1.5">
@@ -133,9 +78,7 @@ export default function MediaPage() {
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{mediaState.current.durationMs
? `${Math.floor(
mediaState.current.durationMs / 60000,
)}:${String(
? `${Math.floor(mediaState.current.durationMs / 60000)}:${String(
Math.floor(
(mediaState.current.durationMs % 60000) / 1000,
),
@@ -153,13 +96,12 @@ export default function MediaPage() {
</p>
)}
{/* Controls */}
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleStop}>
<Button variant="outline" size="sm" onClick={stop}>
<Square className="size-4 mr-1" />
Stop
</Button>
<Button variant="outline" size="sm" onClick={handleSkip}>
<Button variant="outline" size="sm" onClick={skip}>
<SkipForward className="size-4 mr-1" />
Skip
</Button>
@@ -169,7 +111,7 @@ export default function MediaPage() {
className="w-24"
defaultValue={[mediaState?.musicVolume ?? 0.5]}
value={[mediaState?.musicVolume ?? 0.5]}
onValueChange={handleVolume}
onValueChange={setVolume}
min={0}
max={1}
step={0.05}
@@ -177,7 +119,6 @@ export default function MediaPage() {
</div>
</div>
{/* Queue */}
{mediaState && mediaState.queue.length > 0 && (
<div className="space-y-1.5">
<p className="text-xs text-muted-foreground font-medium">