chore: remove React frontend, rename frontend-leptos to frontend

This commit is contained in:
asepharyana
2026-07-03 23:11:00 +07:00
parent 4390a3da5e
commit 8166023f91
175 changed files with 22 additions and 9817 deletions
-3
View File
@@ -1,3 +0,0 @@
# API/WS endpoints — set these before building
VITE_BE_API_URL=http://localhost:3001
VITE_BE_WS_URL=ws://localhost:3001/ws
+2 -4
View File
@@ -1,5 +1,3 @@
# Backend API URL (default: http://localhost:3001)
# API/WS endpoints — set these before building
VITE_BE_API_URL=http://localhost:3001
# Backend WebSocket URL (default: ws://localhost:3001)
VITE_BE_WS_URL=ws://localhost:3001
VITE_BE_WS_URL=ws://localhost:3001/ws
-753
View File
@@ -1,753 +0,0 @@
# Design Tokens — Bete Frontend
## Table of Contents
1. [CSS Custom Properties (OKLCH)](#1-css-custom-properties-oklch)
2. [Z-Index Registry](#2-z-index-registry)
3. [Spacing & Layout Patterns](#3-spacing--layout-patterns)
4. [Typography](#4-typography)
5. [Animation Keyframes](#5-animation-keyframes)
6. [Animation Utility Classes](#6-animation-utility-classes)
7. [Framer Motion Presets](#7-framer-motion-presets)
8. [GSAP Page Transitions](#8-gsap-page-transitions)
9. [Card Pattern](#9-card-pattern)
10. [Stagger Animation Pattern](#10-stagger-animation-pattern)
11. [Component Patterns & Variants](#11-component-patterns--variants)
12. [Reduced-Motion Handling](#12-reduced-motion-handling)
13. [Known Issues & Technical Debt](#13-known-issues--technical-debt)
---
## 1. CSS Custom Properties (OKLCH)
Defined in `styles.css` on `:root`. All use the OKLCH color space (`lightness chroma hue`). Consumed via `oklch(var(--name))` in CSS and `oklch(var(--name))` in the Tailwind config (`tailwind.config.js`).
### Surface Colors
| Variable | OKLCH Value | Description |
|------------------------|--------------------------|------------------------------------|
| `--background` | `1 0 0` | Page background (white) |
| `--foreground` | `0.141 0.005 285.823` | Body text (near-black) |
| `--card` | `1 0 0` | Card surface (white) |
| `--card-foreground` | `0.141 0.005 285.823` | Card text (near-black) |
| `--border` | `0.92 0.004 286.32` | Default border |
| `--input` | `0.92 0.004 286.32` | Input border (same as `--border`) |
### Interaction Colors
| Variable | OKLCH Value | Description |
|------------------------|--------------------------|------------------------------------|
| `--primary` | `0.623 0.214 259.815` | Primary accent (blue) |
| `--primary-soft` | `0.92 0.04 259.815` | Soft primary background |
| `--primary-foreground` | `0.97 0.014 254.604` | Text on primary (near-white) |
| `--secondary` | `0.967 0.001 286.375` | Secondary surface (light gray) |
| `--secondary-foreground`| `0.21 0.006 285.885` | Text on secondary |
| `--muted` | `0.967 0.001 286.375` | Muted surface (same as secondary) |
| `--muted-foreground` | `0.552 0.016 285.938` | Muted text (medium gray) |
| `--accent` | `0.967 0.001 286.375` | Accent surface |
| `--accent-foreground` | `0.21 0.006 285.885` | Text on accent |
| `--destructive` | `0.577 0.245 27.325` | Destructive action (reddish) |
| `--destructive-foreground`| `0.97 0.014 254.604` | Text on destructive |
### Semantic / Effect Colors
| Variable | OKLCH Value | Description |
|--------------------------|-------------------------------|--------------------------------------|
| `--ring` | `0.623 0.214 259.815` | Focus ring (same as primary) |
| `--radius` | `1rem` | Border radius base |
| `--primary-glow` | `0.623 0.214 259.815 / 0.15` | Primary glow backdrop-filter orb |
| `--accent-glow` | `0.552 0.016 285.938 / 0.15` | Accent glow |
| `--card-shadow` | `0.92 0.004 286.32 / 0.3` | Card box-shadow color |
### Tailwind Config Mapping
All custom properties are wired into the Tailwind theme under `theme.extend.colors`:
```js
colors: {
border: "oklch(var(--border))",
input: "oklch(var(--input))",
ring: "oklch(var(--ring))",
background: "oklch(var(--background))",
foreground: "oklch(var(--foreground))",
"primary-soft":"oklch(var(--primary-soft))",
"primary-glow":"oklch(var(--primary-glow))",
"accent-glow": "oklch(var(--accent-glow))",
primary: { DEFAULT: "oklch(var(--primary))", foreground: "oklch(var(--primary-foreground))" },
secondary: { DEFAULT: "oklch(var(--secondary))", foreground: "oklch(var(--secondary-foreground))" },
muted: { DEFAULT: "oklch(var(--muted))", foreground: "oklch(var(--muted-foreground))" },
accent: { DEFAULT: "oklch(var(--accent))", foreground: "oklch(var(--accent-foreground))" },
destructive: { DEFAULT: "oklch(var(--destructive))", foreground: "oklch(var(--destructive-foreground))" },
card: { DEFAULT: "oklch(var(--card))", foreground: "oklch(var(--card-foreground))" },
},
borderRadius: {
lg: "var(--radius)", // 1rem
md: "calc(var(--radius) - 2px)", // 0.875rem
sm: "calc(var(--radius) - 4px)", // 0.75rem
},
```
### Color vs. Variable Hardcoding
**Prefer CSS variables**. The following are supposed to use CSS vars but are **hardcoded to Tailwind utility colors** — see [Known Issues §13.1](#131-hardcoded-utility-colors--no-css-var).
---
## 2. Z-Index Registry
All stacking contexts in the application, inventoried from `*.tsx` and `*.css` files.
| Value | Owner | Context / Location |
|-----------|--------------------------|-------------------------------------------------|
| `-1` | `ParticleBackground.tsx` | Inline `style={{ zIndex: -1 }}` — decorative orbs sit behind all content |
| `10` | `Header.tsx` | Class `sticky top-0 z-10` — sticky page header |
| `50` | `Sidebar.tsx` line 110 | Class `relative z-50` — mascot chatbot toggle button |
| `50` | `MobileTabBar.tsx` | Class `fixed bottom-0 left-0 right-0 z-50` — mobile bottom nav |
| `50` | `toast.tsx` | Class `fixed bottom-4 right-4 z-50` — toast notification container |
| `[9999]` | `Sidebar.tsx` line 122 | Class `fixed bottom-[170px] left-[80px] z-[9999]` — MascotChatbot floating panel |
### Stacking Order (bottom to top)
1. **Layer -1**: Particle background orbs (`ParticleBackground`)
2. **Layer 0**: Main page content, sidebar, cards
3. **Layer 10**: Sticky header (`Header`)
4. **Layer 50**: Toast container, mobile tab bar, mascot toggle button
5. **Layer 9999**: Mascot chatbot floating panel
**Note**: There is no established z-index scale. Values 20, 30, 40 are unused. The arbitrary `z-[9999]` for the chatbot is an outlier — future additions should use a defined scale (e.g., 10/20/30/40/50/100) rather than arbitrary large numbers.
---
## 3. Spacing & Layout Patterns
### Page Layout
```
DashboardLayout
├── ParticleBackground (fixed inset-0)
├── grid-pattern overlay (fixed inset-0, pointer-events-none)
└── flex container (relative flex min-h-screen)
├── Sidebar (shrink-0, w-16 collapsed / w-64 expanded, hidden on <md)
└── main (flex-1, min-w-0)
├── Header (sticky top-0 z-10, px-4 md:px-8)
│ └── flex items-center justify-between
│ ├── h1 + subtitle (left)
│ └── WS/Voice badges (right)
│ (under md) MobileTabBar (fixed bottom-0 z-50)
└── main content (flex-1 overflow-auto, p-4 md:p-6 lg:p-8)
```
### Common Padding Values
| Context | Class Pattern | Notes |
|------------------|-----------------------------|------------------------------|
| Card container | `p-6` | CardContent / CardHeader |
| Card content | `p-6 pt-0` (or `p-4`) | `pt-0` when following header |
| Card footer | `p-6 pt-0` + `flex` | |
| Page content | `p-4 md:p-6 lg:p-8` | Responsive scaling |
| Header | `px-4 py-4 md:px-8` | |
| Sidebar | `px-2` (nav items) | |
| Sub-panels | `p-4` | Music, Screen, etc. |
| Mascot chat | `p-4` (messages), `p-3` (input) | |
### Common Gap Values
| Pattern | Usage |
|----------------|--------------------------------------------|
| `gap-3` | Sidebar nav items, message card avatar+text, recording items, feed groups |
| `gap-4` | Stat card grid (>sm), user card 2-col, user detail layout |
| `gap-6` | Top-level panel sections (dashboard, live) |
| `gap-2` | Filter badges, button groups, message row indicators, toast container |
| `space-y-1` | Compact text+value pairs |
| `space-y-2` | Active speakers list, recording list, message metadata blocks |
| `space-y-3` | Message feed items, skeleton groups |
| `space-y-4` | Auth form, music panel |
| `space-y-6` | User profile detail sections |
### Responsive Grid Breakpoints
- `sm:grid-cols-2` — stat cards, user cards, recordings cards
- `lg:grid-cols-4` — stat summary cards
- `xl:grid-cols-3`, `2xl:grid-cols-4` — image grid
- `xl:grid-cols-[1fr_320px]` — live audio + sidebar layout
- `md:grid-cols-2` — voice connection guild/channel selects
- `md:grid-cols-3` — moderation queue stat cards
---
## 4. Typography
### Font Family
```css
font-family: Poppins, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
```
Defined in `styles.css` and `tailwind.config.js` under `theme.extend.fontFamily.sans`.
### Type Scale
| Class | Used For |
|----------------|--------------------------------------------|
| `text-[10px]` | Badge text, filter labels, status chips |
| `text-[11px]` | Message timestamps, edited/deleted indicators, action hints |
| `text-[12px]` | AI analysis body text |
| `text-xs` | Captions, descriptions, metadata, secondary info, badge text |
| `text-sm` | Body text, card descriptions, message content, form labels |
| `text-base` | Card titles (sometimes), live audio heading |
| `text-lg` | Section headings, stat values |
| `text-xl` | Page title (`h1`) |
| `text-2xl` | Stat card values, moderation queue numbers |
### Font Weights
| Weight | Usage |
|-----------------|--------------------------------------------|
| `font-medium` | Sidebar items, timestamps, badge text, section labels, queue items |
| `font-semibold` | CardTitle, user names, tracking-tight headings |
| `font-bold` | Page titles, stat values, key numbers |
### Tracking
- `tracking-tight`: CardTitle, page `h1`
- `tracking-wider`: Image grid kind badges
### Font Mono
- `font-mono`: Channel IDs, user IDs
---
## 5. Animation Keyframes
### Defined in `styles.css`
```css
@keyframes bar-pulse {
0%, 100% { transform: scaleY(0.8); }
50% { transform: scaleY(1.2); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
```
### Defined in `tailwind.config.js` only (`glowPulse`)
```js
keyframes: {
glowPulse: {
"0%, 100%": { opacity: "0.4" },
"50%": { opacity: "0.8" },
},
},
```
**Note**: `glowPulse` is NOT defined in `styles.css`. It only exists in `tailwind.config.js` and is used by `ParticleBackground.tsx` via the class `animate-glow-pulse`. This works because Tailwind 4 uses the JS config for keyframes, but the CSS file is the canonical keyframe source. This is a **latent inconsistency** — see Known Issues §13.3.
### Animation Timings Summary
| Keyframe | Duration | Timing Function | Used In |
|----------------|----------|------------------------|--------------------------------|
| bar-pulse | 0.4s | ease-in-out | AudioVisualizer (via CSS class)|
| shimmer | 1.5s (CSS) / 2s (TW config) | ease-in-out | Skeleton loading components |
| fadeInUp | 0.5s | ease-out | Utility class `.animate-fade-in-up` |
| fadeIn | 0.3s | ease-out | Utility class `.animate-fade-in` |
| glowPulse | 3s | ease-in-out | ParticleBackground glow orbs |
**Mismatch**: `shimmer` duration is `1.5s` in `styles.css` but `2s` in `tailwind.config.js`. The CSS class `.animate-shimmer` is used by `Skeleton.tsx`, so the CSS definition wins.
---
## 6. Animation Utility Classes
Defined in `styles.css` under `@layer utilities`:
| Class | Animation | Purpose |
|----------------------|------------------------------|--------------------------------|
| `.animate-fade-in-up`| `fadeInUp 0.5s ease-out` | Entry animation for elements |
| `.animate-fade-in` | `fadeIn 0.3s ease-out` | Simple fade-in |
| `.animate-bar-pulse` | `bar-pulse 0.4s ease-in-out infinite` | Audio visualizer bars (transform-origin: bottom) |
| `.animate-shimmer` | `shimmer 1.5s ease-in-out infinite` | Skeleton loading placeholder (gradient sweep) |
Tailwind config also registers `animate-glow-pulse` (`glowPulse 3s ease-in-out infinite`).
**Usage**: `Skeleton` component uses `animate-shimmer`. Inline `animate-spin` (Tailwind built-in) is used for loading spinners on re-analyze buttons.
---
## 7. Framer Motion Presets
All defined in `shared/hooks/useFramerStagger.ts`.
### `cardStagger` — Parent container variant
```ts
{
initial: { opacity: 0 },
animate: {
opacity: 1,
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
},
}
```
### `cardItem` — Child item (fade + slide up)
```ts
{
initial: { opacity: 0, y: 20 },
animate: {
opacity: 1, y: 0,
transition: { duration: 0.4, ease: "easeOut" },
},
}
```
### `fadeSlideUp` — Single element (cubic-bezier)
```ts
{
initial: { opacity: 0, y: 24 },
animate: {
opacity: 1, y: 0,
transition: { duration: 0.5, ease: [0.25, 0.46, 0.45, 0.94] },
},
exit: { opacity: 0, y: -12, transition: { duration: 0.2 } },
}
```
Used by `Header.tsx` (key=activeTab) and `DashboardLayout.tsx` (main content area, key=activeTab).
### `fadeIn` — Simple opacity
```ts
{
initial: { opacity: 0 },
animate: { opacity: 1, transition: { duration: 0.3 } },
exit: { opacity: 0, transition: { duration: 0.2 } },
}
```
### `scaleIn` — Badge/pill entrance
```ts
{
initial: { opacity: 0, scale: 0.8 },
animate: { opacity: 1, scale: 1, transition: { duration: 0.3, ease: "backOut" } },
}
```
### `springUp` — Emphasized entrance
```ts
{
initial: { opacity: 0, y: 30 },
animate: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 200, damping: 20 } },
}
```
### Usage Pattern
```tsx
<motion.div variants={cardStagger} initial="initial" animate="animate">
<motion.div variants={cardItem}>...</motion.div>
<motion.div variants={cardItem}>...</motion.div>
</motion.div>
```
---
## 8. GSAP Page Transitions
### `useGsapTransition` (`shared/hooks/useGsapTransition.ts`)
A custom hook for tab-level page transitions, used by `AuthOverlay.tsx`.
**Page enter animation**:
- Container: opacity 0→1, y 20→0, `duration: 0.4`, `ease: "power2.out"`
- Stagger children: querySelectorAll `[data-stagger]`, `duration: 0.3`, `stagger: 0.05`
- Reduced motion: all durations set to 0
**Page exit animation** (returns Promise):
- Container: opacity→0, y→20, `duration: 0.3`, `ease: "power2.in"`
**Card hover helper** (`gsapCardHover`):
```ts
onMouseEnter: gsap.to(target, { y: -4, boxShadow: "0 8px 25px rgba(0,0,0,0.15)", duration: 0.2 })
onMouseLeave: gsap.to(target, { y: 0, boxShadow: "0 2px 8px rgba(0,0,0,0.08)", duration: 0.2 })
```
(Exported but unused in current components — kept as utility.)
---
## 9. Card Pattern
### Base `Card` component (`shared/ui/card.tsx`)
```
rounded-xl border border-border bg-card text-card-foreground shadow-sm hover:shadow-md transition-shadow
```
### Composition
| Sub-component | Classes | Notes |
|----------------|------------------------------|--------------------------------|
| `Card` | See above | `transition-shadow` for hover |
| `CardHeader` | `flex flex-col space-y-1.5 p-6` | |
| `CardTitle` | `font-semibold leading-none tracking-tight` | `h3` element |
| `CardDescription` | `text-sm text-muted-foreground` | `p` element |
| `CardContent` | `p-6 pt-0` | `pt-0` to collapse with header |
| `CardFooter` | `flex items-center p-6 pt-0` | |
### Common Card Variants
| Context | Additional Classes |
|------------------|-----------------------------------------------------|
| Stat card | `CardContent p-4` (compact) |
| Dashboard stat | `overflow-hidden` |
| Message card | `group hover:border-primary/30 hover:shadow-md` |
| Auth card | `w-full max-w-md border-primary/30 shadow-lg shadow-primary/10` |
| Error/empty card | `border-dashed border-destructive` (error state) |
| User card | `hover:ring-1 hover:ring-primary/30 cursor-pointer` |
| Media queue item | `rounded-lg border-l-2 border-l-primary border-border` |
### Empty State Pattern
```tsx
<div className="flex flex-col items-center gap-4 py-12">
<MascotImage size="md" className="opacity-60" />
<p className="text-sm text-muted-foreground">No data to display</p>
</div>
```
Wrapped in `EmptyStateMascot` component. Also used via standalone icon pattern:
```tsx
<div className="flex flex-col items-center gap-4 py-20 text-muted-foreground">
<BarChart3 className="h-10 w-10" />
<p className="text-sm">No data available yet.</p>
</div>
```
### Skeleton Loading Pattern
```tsx
<Skeleton className="rounded-lg bg-muted animate-shimmer" />
```
Used in `.animate-shimmer` containers matching real card layout. See `MessageCardSkeleton`, `StatsSkeleton`, `UserListSkeleton`, `DetailSkeleton`, `ActiveSpeakersSkeleton`.
---
## 10. Stagger Animation Pattern
### Framer Motion (primary, used in panels)
Applied as `cardStagger` / `cardItem` pair on every major panel:
| Panel | File |
|----------------------|----------------------------------|
| LivePanel | `features/live/index.tsx` |
| MessagesPanel | `features/messages/index.tsx` |
| DashboardStatsContent| `features/dashboard/components/DashboardStats.tsx` |
| UserSummaryList | `features/dashboard/components/UserSummaryList.tsx` |
| UserProfileDetail | `features/dashboard/components/UserProfileDetail.tsx` |
| MessageFeed | `features/messages/components/MessageFeed.tsx` |
**Timing**: stagger 80ms, delayChildren 100ms, item duration 400ms.
### GSAP (used in auth flow)
`useGsapTransition` with `[data-stagger]` attributes. Stagger 50ms, duration 300ms.
---
## 11. Component Patterns & Variants
### Button (`shared/ui/button.tsx`)
**Variants**: `default`, `secondary`, `destructive`, `outline`, `ghost`
**Sizes**: `default` (h-10), `sm` (h-9), `lg` (h-11), `icon` (h-10 w-10)
Base classes:
```
inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium
transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50
```
| Variant | Background |
|--------------|---------------------------------------|
| default | `bg-primary text-primary-foreground shadow-sm hover:bg-primary/90` |
| secondary | `bg-secondary text-secondary-foreground hover:bg-secondary/80` |
| destructive | `bg-destructive text-destructive-foreground hover:bg-destructive/90` |
| outline | `border border-input bg-background hover:bg-accent hover:text-accent-foreground` |
| ghost | `hover:bg-accent hover:text-accent-foreground` |
### Badge (`shared/ui/badge.tsx`)
**Variants**: `default`, `secondary`, `destructive`, `outline`, `success`, `warning`
Base classes:
```
inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors
```
| Variant | Classes (uses CSS vars where possible) |
|------------|------------------------------------------------------|
| default | `bg-primary text-primary-foreground` |
| secondary | `bg-muted text-muted-foreground` |
| destructive| `bg-destructive/15 text-destructive` |
| outline | `border-border text-foreground` |
| success | `bg-emerald-100 text-emerald-700` **[HARDCODED]** |
| warning | `bg-amber-100 text-amber-700` **[HARDCODED]** |
### Input (`shared/ui/input.tsx`)
```
flex h-10 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground
ring-offset-background placeholder:text-muted-foreground
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
disabled:cursor-not-allowed disabled:opacity-50
```
### Select (`shared/ui/select.tsx`)
Same visual treatment as Input (`flex h-10 w-full rounded-lg border border-input bg-background ...`).
### Skeleton (`shared/ui/skeleton.tsx`)
```
rounded-lg bg-muted animate-shimmer
```
### Tabs (`shared/ui/tabs.tsx`)
Wraps Radix `TabsPrimitive`:
| Component | Key Classes |
|---------------|-----------------------------------------------------------|
| `TabsList` | `inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground` |
| `TabsTrigger` | `inline-flex items-center justify-center whitespace-nowrap rounded-lg px-3 py-1.5 text-sm font-medium transition-all ... data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm` |
| `TabsContent` | `mt-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring` |
### Toast (`shared/ui/toast.tsx`)
Context-based system with `addToast` / `removeToast`. Auto-dismiss after 4s.
Container: `fixed bottom-4 right-4 z-50 flex flex-col gap-2`
Type styles:
| Type | Border-left class | Icon color |
|---------|----------------------------------|--------------------|
| info | `border-l-primary` | `text-primary` |
| success | `border-l-emerald-500` | `text-emerald-500` |
| error | `border-l-destructive` | `text-destructive` |
| warning | `border-l-amber-500` | `text-amber-500` |
**Note**: `emerald-500` and `amber-500` are hardcoded Tailwind utilities — see Known Issues.
### Glass Card (utility, `styles.css`)
```css
.glass-card {
@apply bg-white/70 backdrop-blur-sm border border-[oklch(0.92_0.004_286.32)] rounded-xl;
}
```
### Grid Pattern (utility, `styles.css`)
```css
.grid-pattern {
background-image:
linear-gradient(oklch(0.92 0.004 286.32 / 0.3) 1px, transparent 1px),
linear-gradient(90deg, oklch(0.92 0.004 286.32 / 0.3) 1px, transparent 1px);
background-size: 40px 40px;
}
```
Applied in `DashboardLayout.tsx` at `opacity-[0.03]`.
### Gradient Text
```css
.gradient-text {
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400;
}
```
Used in `Header.tsx` for the "IMPHNEN" brand text.
---
## 12. Reduced-Motion Handling
Three layers of protection:
### 1. CSS Level (`styles.css`)
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
### 2. GSAP Hook (`useGsapTransition`)
```ts
function prefersReducedMotion(): boolean {
if (typeof window === "undefined") return false;
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
```
Passes `instant` flag → all durations set to 0, stagger set to 0.
### 3. Particle Background (`ParticleBackground.tsx`)
```tsx
const [reducedMotion, setReducedMotion] = useState(false);
useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
setReducedMotion(mq.matches);
// listener for changes...
}, []);
if (reducedMotion) return null; // Entire particle layer removed
```
### Coverage Gap
Framer Motion `AnimatePresence` and `motion` components (used extensively in MessageFeed, LivePanel, DashboardStats, UserSummaryList, UserProfileDetail, MascotChatbot) do **not** check `prefers-reduced-motion`. They rely solely on the CSS override. While the CSS `!important` rules do suppress Framer Motion animations (since they run via inline styles that get overridden), this is a fragile approach — some Framer Motion `transition` properties (spring physics, stagger delays) may not be fully neutralized by the CSS blanket rule.
---
## 13. Known Issues & Technical Debt
### 13.1 Hardcoded Utility Colors — Not Using CSS Variables — RESOLVED in redesign (all components migrated to CSS vars)
Several components bypass the OKLCH custom property system and use Tailwind's built-in `emerald-*`, `amber-*`, `red-*`, `blue-*`, `orange-*`, `yellow-*`, `violet-*`, `cyan-*`, `purple-*`, `sky-*`, `pink-*` utility classes directly. These will not respond to theme changes.
**Locations**:
| File(s) | Hardcoded Colors Used |
|-------------------------------------------|--------------------------------------------|
| `shared/ui/badge.tsx` | `bg-emerald-100 text-emerald-700` (success variant), `bg-amber-100 text-amber-700` (warning variant) |
| `shared/ui/toast.tsx` | `border-l-emerald-500`, `text-emerald-500`, `border-l-amber-500`, `text-amber-500` |
| `shared/ui/button.tsx` | None (uses CSS vars correctly) |
| `features/dashboard/components/DashboardStats.tsx` | `text-emerald-500`, `bg-emerald-100`, `text-blue-500`, `bg-blue-100`, `text-violet-500`, `bg-violet-100`, `text-emerald-600`, `bg-cyan-100`, `text-cyan-500`, `text-amber-500`, `bg-amber-100` |
| `features/dashboard/components/UserSummaryList.tsx` | `bg-emerald-100 text-emerald-700`, `bg-amber-100 text-amber-700`, `bg-red-100 text-red-700` |
| `features/dashboard/components/UserProfileDetail.tsx` | `bg-red-100 text-red-700`, `bg-emerald-100 text-emerald-700`, `bg-amber-100 text-amber-700`, `text-emerald-600` |
| `features/messages/components/MessageCard.tsx` | `bg-red-100 text-red-700 border-red-200`, `bg-orange-100 text-orange-700 border-orange-200`, `bg-yellow-100 text-yellow-700 border-yellow-200`, `bg-blue-100 text-blue-700 border-blue-200`, `bg-emerald-50/40`, `border-l-emerald-400`, `bg-pink-50/40`, `border-l-pink-400`, `text-pink-600`, `text-pink-600/70` |
| `features/messages/components/MessagesPanel.tsx` | `bg-emerald-100 text-emerald-700 border-emerald-200`, `bg-orange-100 text-orange-700 border-orange-200`, `bg-red-100 text-red-700 border-red-200`, `text-emerald-600` |
| `features/messages/components/ImageGrid.tsx` | `bg-purple-100 text-purple-700 border-purple-200` |
| `features/live/components/ActiveSpeakers.tsx` | `text-emerald-700` |
| `features/live/components/RecordingsSubPanel.tsx` | `border-sky-200 bg-white` |
| `features/live/components/NowPlaying.tsx` | (uses Badge `success`/`warning` variants, which are hardcoded) |
| `widgets/Header.tsx` | `bg-emerald-400`, `bg-red-400`, `bg-gray-400`, `text-emerald-400`, `text-red-400` |
**Fix**: Replace with CSS variable-based tokens, e.g.:
- `emerald-100``oklch(var(--success-bg))` (would need new custom property)
- `text-emerald-700``oklch(var(--success-fg))`
- `border-emerald-200``oklch(var(--success-border))`
### 13.2 `formatTimeAgo` Forces Re-render on Every Tick
In `features/messages/components/MessageCard.tsx`:
```ts
function formatTimeAgo(ts: number): string {
const seconds = Math.floor((Date.now() - ts) / 1000);
// ...
}
```
`Date.now()` is called on every render, not via a periodic timer. This means the "X s ago" / "X m ago" labels are only accurate at the moment of render and do not update reactively. No `useEffect` interval keeps them fresh. Once rendered, a "5s ago" label will stay stale until a parent re-render. This is acceptable for a message feed that re-renders on new data, but inaccurate for pinned/static views.
**Fix**: Either (a) accept staleness (current behavior, simplest), (b) add a `useEffect` interval that forces re-render every ~30s, or (c) use `useSyncExternalStore` with a global ticker.
### 13.3 `AudioVisualizer.tsx` — Hardcoded IMPHNEN Gradient
```ts
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, "#23a1eb");
gradient.addColorStop(1, "#3eb0f2");
```
These hex values (IMPHNEN blue `#23a1eb``#3eb0f2`) are hardcoded and do not reference the OKLCH `--primary` CSS variable. A theme change would not affect the visualizer.
**Fix**: Read CSS custom property at paint time:
```ts
const primaryColor = getComputedStyle(document.documentElement)
.getPropertyValue("--primary").trim();
// Convert OKLCH to hex or use Canvas oklch() if available
```
### 13.4 `glow-pulse` Keyframe Fragmentation — RESOLVED in T02 (moved to styles.css)
The `glowPulse` keyframes are defined in `tailwind.config.js` but NOT in `styles.css`. The class `animate-glow-pulse` is referenced by `ParticleBackground.tsx`. Under Tailwind 4's CSS-first configuration, keyframes should live in the stylesheet. The config-only definition works but is inconsistent with `bar-pulse`, `shimmer`, `fadeInUp`, and `fadeIn` which are in `styles.css`.
**Fix**: Move `@keyframes glowPulse { ... }` into `styles.css`.
### 13.5 `shimmer` Duration Mismatch — RESOLVED in T02 (CSS canonical at 1.5s)
`styles.css`: `animation: shimmer 1.5s ease-in-out infinite`
`tailwind.config.js`: `shimmer: "shimmer 2s linear infinite"` (also uses `linear` vs. `ease-in-out`)
The CSS class `.animate-shimmer` (used by `Skeleton.tsx`) references the CSS-based keyframe, so `1.5s ease-in-out` is the actual runtime value. The Tailwind config value is dead unless used via `animate-shimmer` as a Tailwind class name — which maps to `shimmer 2s linear infinite`.
**Fix**: Align both sources. Pick one canonical definition.
### 13.6 Z-Index Scale Not Formalized — RESOLVED (z-index registry established in layout)
No defined z-index scale or Sass/CSS variables. Values jump from 50 to 9999. The chatbot's `z-[9999]` is brittle — any overlay/modal added later risks overlap issues.
**Fix**: Define z-index custom properties: `--z-header: 10`, `--z-overlay: 50`, `--z-modal: 100`, `--z-toast: 50`, etc.
### 13.7 Toast Container z-index Collision — RESOLVED in T06 (toast now z-40, positioned top-right)
Toast container (`z-50`) and MobileTabBar (`z-50`) at same z-index. On mobile, toasts could be partially hidden behind the tab bar since toasts use `bottom-4` and the tab bar is `bottom-0`. In practice the toast gap prevents overlap, but this is fragile.
### 13.8 Retro `bg-white` Usage
`RecordingsSubPanel.tsx` uses `bg-white` (line 86) instead of `bg-card`. This will not respect a dark theme if one is added.
---
## 14. Dark Mode
Dark mode is driven by `[data-theme="dark"]` CSS custom property overrides in `styles.css`.
The theme is managed by `useTheme()` hook (`shared/hooks/useTheme.ts`).
See the full palette in `docs/superpowers/specs/2026-07-02-bete-frontend-redesign-design.md`.
---
## Appendix: File Map
| Token / Concern | Canonical Source |
|---------------------------|----------------------------------------------|
| CSS custom properties | `styles.css` |
| Tailwind theme extension | `tailwind.config.js` |
| Framer Motion variants | `shared/hooks/useFramerStagger.ts` |
| GSAP transition hook | `shared/hooks/useGsapTransition.ts` |
| `cn()` utility | `shared/lib/utils.ts` |
| Card component | `shared/ui/card.tsx` |
| Button component | `shared/ui/button.tsx` |
| Badge component | `shared/ui/badge.tsx` |
| Input component | `shared/ui/input.tsx` |
| Select component | `shared/ui/select.tsx` |
| Skeleton component | `shared/ui/skeleton.tsx` |
| Tabs component | `shared/ui/tabs.tsx` |
| Toast component | `shared/ui/toast.tsx` |
| ScrollArea component | `shared/ui/scroll-area.tsx` |
| ParticleBackground | `widgets/particles/ParticleBackground.tsx` |
| DashboardLayout | `widgets/DashboardLayout.tsx` |
-17
View File
@@ -1,17 +0,0 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#23a1eb" />
<title>IMPHNEN — Discord Moderation</title>
<link rel="icon" type="image/svg+xml" href="https://raw.githubusercontent.com/IMPHNEN/imphnen-frontend-service/develop/docs/logo.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-45
View File
@@ -1,45 +0,0 @@
# Frontend web application
language: typescript
tasks:
dev:
command: vite --host 0.0.0.0
preset: server
build:
script: 'tsc --noEmit && NODE_NO_WARNINGS=1 vite build'
options:
runFromWorkspaceRoot: false
inputs:
- src
- tsconfig.json
- vite.config.mts
- index.html
outputs:
- dist
preview:
command: vite preview --host 0.0.0.0 --port 3000
preset: server
typecheck:
command: tsc --noEmit
options:
runFromWorkspaceRoot: false
inputs:
- src
- tsconfig.json
lint:
command: biome check --diagnostic-level=error src/
options:
runFromWorkspaceRoot: false
inputs:
- src
format:
command: biome format --write src/
options:
runFromWorkspaceRoot: false
inputs:
- src
-38
View File
@@ -1,38 +0,0 @@
{
"name": "@gmw/frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "tsc --noEmit && NODE_NO_WARNINGS=1 vite build",
"preview": "vite preview --host 0.0.0.0 --port 3000",
"typecheck": "tsc --noEmit",
"lint": "biome check --diagnostic-level=error src/",
"format": "biome format --write src/"
},
"dependencies": {
"@bete/shared": "workspace:*",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"clsx": "^2.1.1",
"framer-motion": "^12.4.0",
"lucide-react": "^1.16.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@biomejs/biome": "latest",
"@tailwindcss/postcss": "^4.3.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"autoprefixer": "^10.5.0",
"postcss": "^8.5.14",
"tailwindcss": "^4.3.0",
"typescript": "^5.9.3",
"vite": "^8.0.13"
}
}
-5
View File
@@ -1,5 +0,0 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
-44
View File
@@ -1,44 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1a1a2e"/>
<stop offset="100%" stop-color="#16213e"/>
</linearGradient>
<linearGradient id="shield" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0ea5e9"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="eye" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#22d3ee"/>
<stop offset="100%" stop-color="#67e8f9"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="128" height="128" rx="24" fill="url(#bg)"/>
<!-- Shield shape -->
<path d="M64 18 L102 32 L102 64 C102 88 84 106 64 114 C44 106 26 88 26 64 L26 32 Z"
fill="url(#shield)" opacity="0.9"/>
<!-- Shield inner border -->
<path d="M64 24 L96 36 L96 63 C96 84 80 100 64 108 C48 100 32 84 32 63 L32 36 Z"
fill="none" stroke="#38bdf8" stroke-width="1.5" opacity="0.6"/>
<!-- Eye outer -->
<ellipse cx="64" cy="60" rx="22" ry="14" fill="none" stroke="#0f172a" stroke-width="2.5" opacity="0.8"/>
<!-- Eye inner -->
<ellipse cx="64" cy="60" rx="20" ry="12" fill="#0f172a" opacity="0.7"/>
<!-- Pupil -->
<circle cx="64" cy="60" r="7" fill="url(#eye)" filter="url(#glow)"/>
<!-- Pupil inner dot -->
<circle cx="64" cy="60" r="3" fill="#ffffff" opacity="0.9"/>
<!-- Eyebrow / scan line -->
<line x1="42" y1="60" x2="86" y2="60" stroke="#0ea5e9" stroke-width="0.5" opacity="0.4"/>
<!-- GMW text -->
<text x="64" y="92" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif"
font-weight="900" font-size="16" fill="#ffffff" letter-spacing="3" opacity="0.95">GMW</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

-300
View File
@@ -1,300 +0,0 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ActiveSpeaker } from "./entities/voice/types.js";
import { AuthOverlay } from "./features/auth";
import { DashboardPanel } from "./features/dashboard";
import { LivePanel } from "./features/live";
import { useMediaControl } from "./features/live/hooks/useMediaControl";
import { useVoiceControl } from "./features/live/hooks/useVoiceControl";
import { MessagesPanel } from "./features/messages";
import { ModerationAlertListener } from "./features/messages/components/ModerationAlertListener";
import {
mergeMessages,
useMessages,
} from "./features/messages/hooks/useMessages";
import { getAppConfig } from "./shared/api/client";
import { useAudioPlayback } from "./shared/hooks/useAudioPlayback";
import { useAudioTransmit } from "./shared/hooks/useAudioTransmit";
import { useTheme } from "./shared/hooks/useTheme";
import { useUIState } from "./shared/hooks/useUIState";
import { MobileTabBar } from "./shared/ui/MobileTabBar";
import { useDashboardSocket } from "./shared/ws/socket";
import { DashboardLayout } from "./widgets/DashboardLayout";
export default function App() {
const { uiState, patchUIState } = useUIState();
const [authenticated, setAuthenticated] = useState(() => {
return sessionStorage.getItem("admin-password") !== null;
});
useTheme();
const handleAuthenticated = useCallback(() => {
setAuthenticated(true);
}, []);
const voice = useVoiceControl();
const media = useMediaControl();
const messages = useMessages();
const [activeSpeakers, setActiveSpeakers] = useState<
(ActiveSpeaker & { heardAt?: number })[]
>([]);
const [monitorGuildId, setMonitorGuildId] = useState("");
const audio = useAudioPlayback();
const isPublicDashboard = import.meta.env.VITE_DASHBOARD_IS_PUBLIC === "true";
const activeTab = uiState.activeTab || "messages";
// Reset persisted tab to "messages" once on mount if not authenticated
// Prevents localStorage carryover from a prior session on the Live tab
useEffect(() => {
if (!authenticated && !isPublicDashboard && uiState.activeTab === "live") {
patchUIState({ activeTab: "messages" });
}
// Run only on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const selectedVoiceGuild =
uiState.selectedVoiceGuild || uiState.selectedGuild || "";
// Resolve monitor guild name from the full guild list (has real names now)
const monitorGuildName = useMemo(
() =>
monitorGuildId
? (voice.guilds.find((g) => g.id === monitorGuildId)?.name ?? null)
: null,
[monitorGuildId, voice.guilds],
);
// Update speaker list from incremental voice_active_user events
const updateSpeakerList = (
prev: (ActiveSpeaker & { heardAt?: number })[],
data: Partial<ActiveSpeaker> & {
userId?: string;
id?: string;
speaking: boolean;
},
): (ActiveSpeaker & { heardAt?: number })[] => {
const key = data.userId ?? data.id;
if (!key) return prev;
const now = Date.now();
const idx = prev.findIndex((s) => (s.userId ?? s.id) === key);
if (idx >= 0) {
const next = [...prev];
next[idx] = { ...next[idx], ...data, heardAt: now };
return next;
}
return [
...prev,
{ ...data, heardAt: now } as ActiveSpeaker & { heardAt?: number },
];
};
const socket = useDashboardSocket({
onBinary: (d) => audio.handleIncomingBinary(d),
onUserState: (users) =>
setActiveSpeakers(
users.map((u) => ({
...u,
heardAt: Date.now(),
})),
),
onVoiceActiveUser: (data) => {
if (data.userId) audio.registerUserId(data.userId);
setActiveSpeakers((prev) =>
updateSpeakerList(prev, {
userId: data.userId,
username: data.username,
avatar: data.avatar,
speaking: data.speaking,
}),
);
},
onVoiceRecordingStarted: () =>
window.dispatchEvent(new CustomEvent("voice_recording_uploaded")),
onVoiceRecordingStopped: () =>
window.dispatchEvent(new CustomEvent("voice_recording_uploaded")),
onMessageCreated: (m) =>
messages.setMessages((prev) => mergeMessages(prev, [m])),
onMessageUpdated: (m) =>
messages.setMessages((prev) =>
prev.map((i) => (i.id === m.id ? { ...i, ...m } : i)),
),
onMessageDeleted: (m) =>
messages.setMessages((prev) =>
prev.map((i) =>
i.id === m.id ? { ...i, type: "deleted" as const } : i,
),
),
onMessageAnalyzed: (msg) => {
messages.setMessages((prev) => mergeMessages(prev, [msg]));
const status = msg.ai_status;
if (status === "flagged") {
const username = msg.username || msg.user_id || "unknown";
const severity = msg.ai_severity || "";
const categories = msg.ai_categories || "";
const brief = msg.ai_analysis?.slice(0, 80) ?? "Message flagged by AI";
window.dispatchEvent(
new CustomEvent("moderation_alert", {
detail: { type: status, username, severity, categories, brief },
}),
);
}
},
onAttachmentUploaded: () =>
messages
.fetchMessages(monitorGuildId || undefined)
.catch(() => undefined),
onAttachmentCreated: () =>
messages
.fetchMessages(monitorGuildId || undefined)
.catch(() => undefined),
onMediaState: (state) => media.setMediaState(state),
onVoiceRecordingUploaded: (d) =>
window.dispatchEvent(
new CustomEvent("voice_recording_uploaded", { detail: d }),
),
});
const transmit = useAudioTransmit(socket.socketRef);
// Load app config on mount
useEffect(() => {
getAppConfig()
.then((c) => {
if (c.monitorGuildId) {
setMonitorGuildId(c.monitorGuildId);
}
})
.catch(() => undefined);
}, []);
// Load voice channels when guild changes (Live tab)
useEffect(() => {
if (selectedVoiceGuild)
voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined);
}, [selectedVoiceGuild, voice.loadVoiceChannels]);
// Auto-fetch messages for the monitor guild
useEffect(() => {
if (monitorGuildId)
messages.fetchMessages(monitorGuildId).catch(() => undefined);
}, [monitorGuildId, messages.fetchMessages]);
// Periodic refetch — keeps dashboard in sync even if WS events missed
useEffect(() => {
if (!monitorGuildId) return;
const interval = setInterval(() => {
messages.fetchMessages(monitorGuildId).catch(() => undefined);
}, 15_000);
return () => clearInterval(interval);
}, [monitorGuildId, messages.fetchMessages]);
// Stale speaker pruning — remove speakers not heard from in 30s
useEffect(() => {
const interval = setInterval(() => {
setActiveSpeakers((prev) => {
const now = Date.now();
const pruned = prev.filter(
(s) => s.speaking || (s.heardAt && now - s.heardAt < 30_000),
);
return pruned.length < prev.length ? pruned : prev;
});
}, 30_000);
return () => clearInterval(interval);
}, []);
// Push-to-Talk — hold Space to transmit
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
)
return;
if (e.code === "Space" && !transmit.isStreaming && e.repeat === false) {
e.preventDefault();
transmit.startTransmit().catch(() => undefined);
}
};
const handleKeyUp = (e: KeyboardEvent) => {
if (e.code === "Space" && transmit.isStreaming) {
transmit.stopTransmit();
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, [transmit]);
return (
<DashboardLayout
activeTab={activeTab}
wsStatus={socket.status}
voiceStatus={voice.voiceStatus}
onTabChange={(tab) => patchUIState({ activeTab: tab })}
recentMessages={messages.messages}
guildId={monitorGuildId}
channelId={
uiState.selectedTextChannel || uiState.selectedVoiceChannel || undefined
}
>
{activeTab === "live" && !authenticated && !isPublicDashboard ? (
<AuthOverlay onAuthenticated={handleAuthenticated} />
) : activeTab === "live" ? (
<LivePanel
guilds={voice.guilds}
voiceChannels={voice.voiceChannels}
selectedGuild={selectedVoiceGuild}
selectedChannel={uiState.selectedVoiceChannel || ""}
micLevel={0}
status={voice.voiceStatus}
voiceLoading={voice.loading}
activeSpeakers={activeSpeakers}
levels={audio.levels}
isListening={audio.isListening}
isStreaming={transmit.isStreaming}
mediaState={media.mediaState}
mediaLoading={media.loading}
onGuildChange={(id) =>
patchUIState({ selectedVoiceGuild: id, selectedVoiceChannel: "" })
}
onChannelChange={(id) => patchUIState({ selectedVoiceChannel: id })}
onJoin={() =>
voice.joinVoice(
selectedVoiceGuild,
uiState.selectedVoiceChannel || "",
)
}
onDisconnect={() => voice.leaveVoice()}
onListenToggle={audio.toggleListening}
onStreamingToggle={transmit.toggle}
onQueueMusic={(s) => media.enqueue(s, "music")}
onStartScreen={(s) => media.enqueue(s, "screen")}
onSkip={media.skip}
onStop={media.stop}
onVolumeChange={media.setVolume}
/>
) : activeTab === "dashboard" ? (
<DashboardPanel />
) : (
<MessagesPanel
guildName={monitorGuildName}
messages={messages.messages}
onReanalyze={messages.reanalyze}
onReanalyzeAllErrors={messages.reanalyzeAllErrors}
onLoadMore={messages.loadMore}
hasMore={messages.hasMore}
loadingMore={messages.loadingMore}
/>
)}
<MobileTabBar
activeTab={activeTab}
onTabChange={(tab) => patchUIState({ activeTab: tab })}
/>
<ModerationAlertListener />
</DashboardLayout>
);
}
@@ -1,75 +0,0 @@
export interface DashboardStats {
total_messages: number;
total_users: number;
total_flagged: number;
total_clean: number;
total_warned: number;
total_error: number;
total_voice_recordings: number;
total_profiles: number;
today_messages: number;
today_flagged: number;
active_users_24h: number;
top_channels: Array<{
channel_id: string;
channel_name: string | null;
message_count: number;
}>;
moderation_overview: {
pending: number;
processing: number;
error: number;
};
}
export interface DashboardUser {
user_id: string;
username: string | null;
avatar_url: string | null;
profile_summary: string | null;
total_messages: number;
flagged_count: number;
last_message_at: number | null;
trust_score: number | null;
}
export interface DashboardUserDetail extends DashboardUser {
last_analyzed_at: number | null;
clean_message_streak: number | null;
total_infractions: number | null;
clean_count: number;
recent_messages: Array<{
id: string;
content: string;
channel_id: string;
created_at: number;
ai_status: string | null;
}>;
}
export interface DashboardChannel {
channel_id: string;
channel_name: string | null;
guild_id: string | null;
total_messages: number;
flagged_count: number;
last_message_at: number | null;
culture_summary: string | null;
last_analyzed_at: number | null;
}
export interface DashboardChannelDetail extends DashboardChannel {
clean_count: number;
recent_messages: Array<{
id: string;
content: string;
channel_id: string;
created_at: number;
ai_status: string | null;
username: string | null;
}>;
}
export interface ChatResponse {
response?: string;
}
@@ -1,19 +0,0 @@
export interface Guild {
id: string;
name: string;
icon: string | null;
}
export interface Channel {
id: string;
name: string;
type?: string;
parentId?: string | null;
}
export interface GuildVoiceEntry {
guildId: string;
channelId: string;
channelName: string;
connectedAt: number;
}
@@ -1,17 +0,0 @@
export type MediaMode = "music" | "screen";
export interface MediaItem {
id?: string;
source: string;
title: string;
mode?: "music" | "screen";
durationMs?: number | null;
thumbnailUrl?: string | null;
}
export interface MediaState {
playing: boolean;
musicVolume: number;
current: MediaItem | null;
queue: MediaItem[];
}
@@ -1,19 +0,0 @@
export type {
AIRecommendedAction,
AISeverity,
AIStatus,
MessageRecord,
PageResult,
} from "@bete/shared";
export interface MessageMetadata {
stickers?: Array<{ name?: string; url?: string }>;
attachments?: Array<{ name: string; url: string; contentType?: string }>;
embeds?: Array<{ title?: string; image?: string; thumbnail?: string }>;
channel?: {
channelId: string;
channelName?: string;
threadId?: string;
threadName?: string;
};
}

Some files were not shown because too many files have changed in this diff Show More