- Fix 98 TypeScript errors across 37 files: - Service layer double-unwrapping (subscriptionService, distributionService, gearService) - Self-referencing variables in SearchPageResults - FeedView/ExploreView .posts→.items alignment - useQueueSync Zustand subscribe API - AdminAuditLogsView missing interface fields - Toast proxy type, interceptor type narrowing - 22 unused imports/variables removed - 5 storybook mock data fixes - Align frontend API calls with backend endpoints: - Analytics: useAnalyticsView now calls /creator/analytics/dashboard (was /analytics) - Chat: chatService uses /conversations (was mock data), WS URL from backend token - Dashboard StatsSection: uses real /dashboard API data (was hardcoded zeros) - Settings: suppress 2FA toast error when endpoint unavailable - Fix marketplace products: seed uses 'active' status (was 'published') - Enrich seed: admin follows all creators (feed has content) - Optimize bundle: vendor catch-all 793KB→318KB gzip (-60%) Split into vendor-charts, vendor-emoji, vendor-swagger, vendor-media, etc. - Clean repo: remove ~100 orphaned screenshots, audit reports, logs from root Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
126 lines
5.5 KiB
TypeScript
126 lines
5.5 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
import { ChatSidebar } from '../components/ChatSidebar';
|
|
import { ChatRoom } from '../components/ChatRoom';
|
|
import { ChatInput } from '../components/ChatInput';
|
|
import { useChatStore } from '../store/chatStore';
|
|
import { useAuthStore } from '@/features/auth/store/authStore';
|
|
import { useUser } from '@/features/auth/hooks/useUser';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { apiClient } from '@/services/api/client';
|
|
import { AlertCircle } from 'lucide-react';
|
|
import { env } from '@/config/env';
|
|
import { cn } from '@/lib/utils';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card } from '@/components/ui/card';
|
|
|
|
export const ChatPage: React.FC = () => {
|
|
const { isAuthenticated } = useAuthStore();
|
|
const { data: user } = useUser();
|
|
const userId = user?.id;
|
|
const { setWsToken, currentConversationId, wsStatus } = useChatStore();
|
|
|
|
const {
|
|
data: wsTokenResponse,
|
|
isLoading: isTokenLoading,
|
|
error: tokenError,
|
|
} = useQuery({
|
|
queryKey: ['chatWsToken', userId],
|
|
queryFn: async () => {
|
|
if (!isAuthenticated || !userId) return null;
|
|
const response = await apiClient.post('/chat/token', {});
|
|
return response.data;
|
|
},
|
|
enabled: isAuthenticated && !!userId && wsStatus === 'disconnected',
|
|
refetchOnWindowFocus: false,
|
|
retry: false,
|
|
staleTime: 5 * 60 * 1000,
|
|
gcTime: 10 * 60 * 1000,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (wsTokenResponse?.token) {
|
|
// Derive the absolute WS URL from the backend-provided ws_url path
|
|
// (e.g. "/api/v1/ws") or fall back to the env-derived WS_URL.
|
|
let wsUrl = env.WS_URL;
|
|
if (wsTokenResponse.ws_url) {
|
|
const backendPath: string = wsTokenResponse.ws_url;
|
|
if (backendPath.startsWith('ws://') || backendPath.startsWith('wss://')) {
|
|
wsUrl = backendPath;
|
|
} else {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
wsUrl = `${protocol}//${window.location.host}${backendPath}`;
|
|
}
|
|
}
|
|
|
|
const needsUpdate =
|
|
wsTokenResponse.token !== useChatStore.getState().wsToken ||
|
|
wsUrl !== useChatStore.getState().wsUrl;
|
|
|
|
if (needsUpdate) {
|
|
setWsToken(wsTokenResponse.token, wsUrl);
|
|
}
|
|
}
|
|
}, [wsTokenResponse, setWsToken]);
|
|
|
|
if (!isAuthenticated) return (
|
|
<div className="flex flex-col items-center justify-center h-layout-chat">
|
|
<Card variant="glass" className="p-8 text-center max-w-md border-primary/20">
|
|
<AlertCircle className="w-12 h-12 text-primary mx-auto mb-4 opacity-50" />
|
|
<h2 className="text-xl font-bold text-foreground mb-2">Access Restricted</h2>
|
|
<p className="mb-6 text-muted-foreground">Encrypted channel access requires authorization.</p>
|
|
<Button onClick={() => (window.location.href = '/login')}>Initialize Handshake</Button>
|
|
</Card>
|
|
</div>
|
|
);
|
|
|
|
if (isTokenLoading || wsStatus === 'connecting') return (
|
|
<div className="flex flex-col items-center justify-center h-layout-chat">
|
|
<div className="relative mb-6">
|
|
<div className="w-16 h-16 border-2 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<div className="w-2 h-2 bg-primary rounded-full animate-pulse shadow-status-dot-cyan" />
|
|
</div>
|
|
</div>
|
|
<p className="font-mono text-sm text-primary animate-pulse tracking-widest">ESTABLISHING UPLINK...</p>
|
|
</div>
|
|
);
|
|
|
|
if (tokenError) return (
|
|
<div className="flex flex-col items-center justify-center h-layout-chat">
|
|
<Card variant="glass" className="p-8 text-center max-w-md border-destructive/30">
|
|
<AlertCircle className="w-12 h-12 text-destructive mb-4" />
|
|
<h2 className="text-xl font-bold text-foreground mb-2">Connection Terminated</h2>
|
|
<p className="text-destructive/80 mb-4">{tokenError instanceof Error ? tokenError.message : 'Secure handshake failed.'}</p>
|
|
<Button variant="outline" onClick={() => window.location.reload()}>Retry Connection</Button>
|
|
</Card>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="h-layout-chat-main flex gap-6 overflow-hidden p-4 container mx-auto max-w-layout-content">
|
|
{/* Sidebar - Glass Panel */}
|
|
<Card variant="glass" className="w-80 shrink-0 flex flex-col overflow-hidden p-0 border-white/5 bg-black/40 backdrop-blur-2xl">
|
|
<div className="p-4 border-b border-white/5 flex items-center justify-between">
|
|
<h3 className="font-bold text-sm tracking-widest text-muted-foreground uppercase">Channels</h3>
|
|
<div className={cn("w-2 h-2 rounded-full", wsStatus === 'connected' ? 'bg-success shadow-status-dot-lime' : 'bg-destructive')} />
|
|
</div>
|
|
<ChatSidebar />
|
|
</Card>
|
|
|
|
{/* Main Chat Area - Holographic Container */}
|
|
<Card variant="glass" className="flex-1 flex flex-col overflow-hidden relative p-0 border-white/5 bg-black/40 backdrop-blur-2xl">
|
|
{/* Background Grid */}
|
|
<div className="absolute inset-0 opacity-[0.03] pointer-events-none"
|
|
style={{ backgroundImage: 'linear-gradient(var(--color-primary) 1px, transparent 1px), linear-gradient(90deg, var(--color-primary) 1px, transparent 1px)', backgroundSize: '40px 40px' }} />
|
|
|
|
<div className="flex-1 overflow-hidden flex flex-col relative z-10">
|
|
<ChatRoom conversationId={currentConversationId || ''} />
|
|
</div>
|
|
|
|
<div className="p-4 border-t border-white/5 bg-black/40 relative z-20 backdrop-blur-xl">
|
|
<ChatInput />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|