refactor(frontend): major rebuild — feature-sliced architecture + bug fixes + glass-morphism UI

Architecture:
- Feature-sliced directory: entities/, shared/, features/, widgets/
- Consolidated API client (shared/api/client.ts) — all endpoints in one file
- Single WebSocket manager (shared/ws/socket.ts) with typed event bus
- Extracted hooks: useAudioPlayback, useAudioTransmit, useLocalStorage, useUIState
- UI primitives moved to shared/ui/ with barrel export
- Added Skeleton, Toast, MobileTabBar components

Bug fixes (8/8):
1. useMemo→useEffect in RecordingsSubPanel (async side-effect anti-pattern)
2. ArrayBuffer.slice() before WebSocket send (shared buffer bug)
3. Proper useEffect dependency arrays throughout
4. Stable React keys (no index fallbacks)
5. onReanalyze properly awaited (Promise<void> return)
6. monitorGuild memoized with useMemo
7. localStorage validation with shape checking
8. Deleted duplicate socket logic (ws/client.ts removed)

UI polish:
- Glass-morphism design tokens (backdrop-blur, translucent cards)
- Gradient mesh background with subtle radial overlays
- Expandable sidebar + mobile bottom tab bar
- Skeleton loading placeholders
- Audio visualizer with CSS pulse animation

Deleted: src/api/, src/components/, src/hooks/, src/types/, src/ws/, src/lib/
Added: 40 new files across feature-sliced structure

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-01 16:42:36 +07:00
co-authored by Claude Opus 4.8
parent 1aab0d1df1
commit f5507e01f6
84 changed files with 2054 additions and 2658 deletions
@@ -0,0 +1,89 @@
import type { HourlyBucket } from "../../../shared/api/client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../../shared/ui";
interface ActivityChartProps {
hourly: HourlyBucket[];
loading: boolean;
}
export function ActivityChart({ hourly, loading }: ActivityChartProps) {
if (loading && !hourly?.length) {
return <LoadingBox />;
}
if (!hourly?.length) {
return <EmptyBox text="Belum ada data untuk periode ini." />;
}
const data = hourly.map((b) => {
const utcHour = parseInt(b.hour.slice(11, 13), 10);
const jakartaHour = (utcHour + 7) % 24;
return {
hour: `${String(jakartaHour).padStart(2, "0")}:00`,
clean: b.clean,
warned: b.warned,
flagged: b.flagged,
error: b.error,
total: b.count,
};
});
return (
<Card className="col-span-1 lg:col-span-2 glass border-white/5">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-semibold">Aktivitas per Jam</CardTitle>
<CardDescription className="text-xs">Distribusi pesan per jam berdasarkan status moderasi.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="grid grid-cols-4 gap-2 text-[10px] uppercase tracking-wider text-muted-foreground">
<span>Clean</span>
<span>Warned</span>
<span>Flagged</span>
<span>Error</span>
</div>
<div className="max-h-55 space-y-2 overflow-auto pr-1">
{data.map((bucket) => {
const total = Math.max(bucket.total, 1);
const clean = bucket.clean / total;
const warned = bucket.warned / total;
const flagged = bucket.flagged / total;
const error = bucket.error / total;
return (
<div key={bucket.hour} className="grid gap-1 rounded-xl border border-border bg-background/50 p-3">
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
<span className="font-medium text-foreground">{bucket.hour}</span>
<span>{bucket.total} pesan</span>
</div>
<div className="flex h-3 overflow-hidden rounded-full bg-muted">
<div className="bg-emerald-500/80" style={{ width: `${clean * 100}%` }} />
<div className="bg-amber-500/80" style={{ width: `${warned * 100}%` }} />
<div className="bg-red-500/80" style={{ width: `${flagged * 100}%` }} />
<div className="bg-orange-500/80" style={{ width: `${error * 100}%` }} />
</div>
</div>
);
})}
</div>
</div>
</CardContent>
</Card>
);
}
function LoadingBox() {
return (
<Card className="col-span-1 flex h-65 items-center justify-center text-sm text-muted-foreground lg:col-span-2 glass border-white/5">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
<span className="ml-2">Memuat data...</span>
</Card>
);
}
function EmptyBox({ text }: { text: string }) {
return (
<Card className="col-span-1 flex h-65 items-center justify-center text-sm text-muted-foreground lg:col-span-2 glass border-white/5">
{text}
</Card>
);
}