From ca0aa9ee9cfb32fdbe21049d64c41b1f6012b785 Mon Sep 17 00:00:00 2001 From: vantablackTea Date: Sun, 27 Sep 2026 20:12:29 +0300 Subject: [PATCH] frontend: now working fully, fuctionality tested --- web/src/App.tsx | 74 +++---- web/src/api.ts | 12 -- web/src/auth/AuthContext.tsx | 3 +- web/src/components/Avatar.tsx | 40 ++++ web/src/components/ChatWindow.tsx | 229 +++++++++++++++++++++ web/src/components/MessageBubble.tsx | 83 ++++++++ web/src/components/MessageList.tsx | 146 +++++++++++++ web/src/components/NewChatDialog.tsx | 208 +++++++++++++++++++ web/src/components/Sidebar.tsx | 222 ++++++++++++++++++++ web/src/components/icons.tsx | 110 ++++++++++ web/src/hooks/useConversations.ts | 28 ++- web/src/hooks/useMessages.ts | 20 +- web/src/main.tsx | 7 +- web/src/screens/AuthScreen.tsx | 253 +++++++++++++++++++++++ web/src/screens/ChatApp.tsx | 296 +++++++++++++++++++++++++++ 15 files changed, 1657 insertions(+), 74 deletions(-) delete mode 100644 web/src/api.ts create mode 100644 web/src/components/Avatar.tsx create mode 100644 web/src/components/ChatWindow.tsx create mode 100644 web/src/components/MessageBubble.tsx create mode 100644 web/src/components/MessageList.tsx create mode 100644 web/src/components/NewChatDialog.tsx create mode 100644 web/src/components/Sidebar.tsx create mode 100644 web/src/components/icons.tsx create mode 100644 web/src/screens/AuthScreen.tsx create mode 100644 web/src/screens/ChatApp.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index 1fe54f4..5dc39a7 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,50 +1,36 @@ -import { useEffect, useState } from 'react' -import { pingApi, type PingResponse } from './api' -import './index.css' - -type ApiState = - | { status: 'loading' } - | { status: 'ok'; data: PingResponse } - | { status: 'error'; error: string } - -function App() { - const [api, setApi] = useState({ status: 'loading' }) - - useEffect(() => { - let cancelled = false - pingApi() - .then((data) => { - if (!cancelled) setApi({ status: 'ok', data }) - }) - .catch((err: unknown) => { - if (!cancelled) setApi({ status: 'error', error: String(err) }) - }) - return () => { - cancelled = true - } - }, []) +import { useAuth } from './auth/AuthContext' +import AuthScreen from './screens/AuthScreen' +import ChatApp from './screens/ChatApp' +function Splash() { return ( -
-

Messenger

-
-
- - - {api.status === 'loading' && 'API: connecting…'} - {api.status === 'ok' && 'API: connected'} - {api.status === 'error' && 'API: unreachable'} - -
- {api.status === 'ok' && ( -

- {api.data.message} · server time {api.data.time} -

- )} - {api.status === 'error' &&

{api.error}

} +
+
+
-
+
+

Пульс

+

Подключаемся…

+
+ ) } -export default App \ No newline at end of file +export default function App() { + const { user, loading } = useAuth() + + if (loading) return + if (!user) return + return +} diff --git a/web/src/api.ts b/web/src/api.ts deleted file mode 100644 index fd51e37..0000000 --- a/web/src/api.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface PingResponse { - message: string - time: string -} - -export async function pingApi(): Promise { - const res = await fetch('/api/v1/ping') - if (!res.ok) { - throw new Error(`API responded with ${res.status}`) - } - return res.json() -} \ No newline at end of file diff --git a/web/src/auth/AuthContext.tsx b/web/src/auth/AuthContext.tsx index e52d781..2c28a57 100644 --- a/web/src/auth/AuthContext.tsx +++ b/web/src/auth/AuthContext.tsx @@ -71,7 +71,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { return {children} } -export function useAuth(): AuthContextValue { +// oxlint-disable-next-line react/only-export-components -- provider and its hook must stay colocated +export const useAuth = (): AuthContextValue => { const context = useContext(AuthContext) if (!context) throw new Error('useAuth должен использоваться внутри AuthProvider') return context diff --git a/web/src/components/Avatar.tsx b/web/src/components/Avatar.tsx new file mode 100644 index 0000000..7f15bc6 --- /dev/null +++ b/web/src/components/Avatar.tsx @@ -0,0 +1,40 @@ +import { avatarColor, initials } from '../lib/format' + +type AvatarSize = 'sm' | 'md' | 'lg' | 'xl' + +interface AvatarProps { + name: string + seed: number | string + size?: AvatarSize + online?: boolean + className?: string +} + +const SIZE_CLASSES: Record = { + sm: 'h-8 w-8 text-[11px]', + md: 'h-10 w-10 text-xs', + lg: 'h-12 w-12 text-sm', + xl: 'h-16 w-16 text-lg', +} + +export default function Avatar({ name, seed, size = 'md', online = false, className = '' }: AvatarProps) { + const background = avatarColor(seed) + return ( + + + {online && ( + + ) +} diff --git a/web/src/components/ChatWindow.tsx b/web/src/components/ChatWindow.tsx new file mode 100644 index 0000000..3d4b3c1 --- /dev/null +++ b/web/src/components/ChatWindow.tsx @@ -0,0 +1,229 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import Avatar from './Avatar' +import MessageList from './MessageList' +import { IconArrowLeft, IconChat, IconSend } from './icons' +import { peerForConversation, userTitle } from '../lib/format' +import type { ClientMessage, Conversation, User } from '../lib/types' + +interface ChatWindowProps { + className?: string + conversation: Conversation | null + currentUser: User + directory: Map + peers: Record + presence: Record + peerIsTyping: boolean + messages: ClientMessage[] + loading: boolean + loadingOlder: boolean + hasMore: boolean + error: string | null + onBack: () => void + onSend: (conversationId: number, body: string) => void + onTyping: (conversationId: number, isTyping: boolean) => void + onLoadOlder: () => void + onRetry: (message: ClientMessage) => void +} + +function TypingDots() { + return ( +