feat: add recordings, settings, and voice pages with WebSocket integration
Deploy to VPS / deploy (push) Failing after 1m36s
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:
@@ -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 {};
|
||||
}
|
||||
}
|
||||
@@ -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"];
|
||||
@@ -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"];
|
||||
Reference in New Issue
Block a user