feat: add recordings, settings, and voice pages with WebSocket integration
Deploy to VPS / deploy (push) Failing after 1m36s

- Implemented RecordingsPage to display and manage voice recordings with live updates via WebSocket.
- Created SettingsPage for user preferences, including theme toggling and server configuration display.
- Developed VoicePage for managing voice connections, including guild and channel selection, and active speaker display.
- Introduced GuildSelector component for selecting Discord guilds with error handling and loading states.
- Added utility functions for formatting numbers and bytes, and safely parsing JSON.
- Established navigation structure for the dashboard with relevant links for new features.
This commit is contained in:
asepharyana
2026-07-26 15:34:08 +07:00
parent eae0d7ce56
commit c2502a0e5f
20 changed files with 1520 additions and 1036 deletions
+47
View File
@@ -0,0 +1,47 @@
/**
* Format a number with locale separators.
*/
export function formatNumber(n: number): string {
return n.toLocaleString();
}
/**
* Format bytes into a human-readable string.
*/
export function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
/**
* Safely parse a JSON string into an array.
*/
export function safeParseJsonArray(
value: string | null | undefined,
): string[] {
if (!value) return [];
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) return parsed;
return [];
} catch {
return [];
}
}
/**
* Safely parse a JSON string into an object.
*/
export function safeParseJsonObject(
value: string | null | undefined,
): Record<string, unknown> {
if (!value) return {};
try {
const parsed = JSON.parse(value);
if (typeof parsed === "object" && parsed !== null) return parsed;
return {};
} catch {
return {};
}
}
+75
View File
@@ -0,0 +1,75 @@
import {
BarChart3,
Headphones,
LayoutDashboard,
MessageSquare,
Mic,
Music,
Search,
type LucideIcon,
} from "lucide-react";
export interface NavItem {
href: string;
label: string;
icon: LucideIcon;
/** The pathname prefix that indicates this item is active */
matchPrefix: string;
}
/**
* Primary navigation items shown in the sidebar.
*/
export const navItems: NavItem[] = [
{
href: "/dashboard",
label: "Dashboard",
icon: LayoutDashboard,
matchPrefix: "/dashboard",
},
{
href: "/messages",
label: "Messages",
icon: MessageSquare,
matchPrefix: "/messages",
},
{
href: "/voice",
label: "Voice",
icon: Mic,
matchPrefix: "/voice",
},
{
href: "/media",
label: "Media",
icon: Music,
matchPrefix: "/media",
},
{
href: "/recordings",
label: "Recordings",
icon: Headphones,
matchPrefix: "/recordings",
},
{
href: "/analysis",
label: "Search",
icon: Search,
matchPrefix: "/analysis",
},
{
href: "/settings",
label: "Settings",
icon: BarChart3,
matchPrefix: "/settings",
},
];
/**
* Mobile bottom bar items (subset of primary nav).
*/
export const mobileNavItems: NavItem[] = navItems.filter((item) =>
["/dashboard", "/messages", "/voice", "/media"].includes(item.href),
);
export type NavItemId = (typeof navItems)[number]["href"];
-9
View File
@@ -1,9 +0,0 @@
import { LayoutDashboard, MessageSquare, Radio } from "lucide-react";
export const tabs = [
{ id: "messages", label: "Messages", icon: MessageSquare },
{ id: "live", label: "Live", icon: Radio },
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
] as const;
export type TabId = (typeof tabs)[number]["id"];