feat(frontend): add routing skeleton with workspace chat and login pages
This commit is contained in:
134
src/App.tsx
134
src/App.tsx
@@ -1,123 +1,19 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import './App.css'
|
||||
|
||||
type Health = { ok: boolean; service: string; database?: string }
|
||||
type MessageItem = {
|
||||
messageId: string
|
||||
seq: number
|
||||
content: string
|
||||
createdAt: string
|
||||
isDeleted?: boolean
|
||||
}
|
||||
|
||||
const API_BASE = import.meta.env.VITE_GUILD_API_BASE ?? 'http://localhost:7002/api'
|
||||
const SOCKET_BASE = import.meta.env.VITE_GUILD_SOCKET_BASE ?? 'http://localhost:7002/realtime'
|
||||
const API_KEY = import.meta.env.VITE_API_KEY ?? 'change-me-api-key'
|
||||
|
||||
function App() {
|
||||
const [health, setHealth] = useState<Health | null>(null)
|
||||
const [channelId, setChannelId] = useState('')
|
||||
const [messages, setMessages] = useState<MessageItem[]>([])
|
||||
const [content, setContent] = useState('')
|
||||
const [socketState, setSocketState] = useState<'offline' | 'online'>('offline')
|
||||
|
||||
const client = useMemo(
|
||||
() =>
|
||||
axios.create({
|
||||
baseURL: API_BASE,
|
||||
headers: { 'x-api-key': API_KEY },
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
client.get('/healthz').then((res) => setHealth(res.data)).catch(() => setHealth(null))
|
||||
}, [client])
|
||||
|
||||
useEffect(() => {
|
||||
if (!channelId) return
|
||||
|
||||
const socket: Socket = io(SOCKET_BASE, {
|
||||
transports: ['websocket'],
|
||||
auth: { apiKey: API_KEY, userId: 'frontend-user' },
|
||||
extraHeaders: { 'x-api-key': API_KEY },
|
||||
})
|
||||
|
||||
socket.on('connect', () => {
|
||||
setSocketState('online')
|
||||
socket.emit('join_channel', { channelId })
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => setSocketState('offline'))
|
||||
socket.on('message.created', (m) => setMessages((prev) => [...prev, m]))
|
||||
socket.on('message.updated', (m) =>
|
||||
setMessages((prev) => prev.map((x) => (x.messageId === m.messageId ? m : x))),
|
||||
)
|
||||
socket.on('message.deleted', (m) =>
|
||||
setMessages((prev) =>
|
||||
prev.map((x) => (x.messageId === m.messageId ? { ...x, isDeleted: true, content: '[deleted]' } : x)),
|
||||
),
|
||||
)
|
||||
|
||||
return () => {
|
||||
socket.emit('leave_channel', { channelId })
|
||||
socket.disconnect()
|
||||
setSocketState('offline')
|
||||
}
|
||||
}, [channelId])
|
||||
|
||||
async function pullMessages() {
|
||||
if (!channelId) return
|
||||
const res = await client.get(`/channels/${channelId}/messages`, {
|
||||
params: { seq_from: 1, seq_to: 999999, limit: 100 },
|
||||
})
|
||||
setMessages(res.data.items ?? [])
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
if (!channelId || !content.trim()) return
|
||||
await client.post(`/channels/${channelId}/messages`, {
|
||||
content,
|
||||
authorUserId: 'frontend-user',
|
||||
})
|
||||
setContent('')
|
||||
}
|
||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||
import AppLayout from './layouts/AppLayout'
|
||||
import ChatPage from './pages/ChatPage'
|
||||
import LoginPage from './pages/LoginPage'
|
||||
import WorkspacePage from './pages/WorkspacePage'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<main className="app">
|
||||
<h1>Fabric Frontend</h1>
|
||||
<p>Health: {health ? `${health.service} (${health.database ?? 'ok'})` : 'unavailable'}</p>
|
||||
<p>Socket: {socketState}</p>
|
||||
|
||||
<section className="row">
|
||||
<input
|
||||
value={channelId}
|
||||
onChange={(e) => setChannelId(e.target.value)}
|
||||
placeholder="Channel ID"
|
||||
/>
|
||||
<button onClick={pullMessages}>拉取消息</button>
|
||||
</section>
|
||||
|
||||
<section className="row">
|
||||
<input
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="输入消息"
|
||||
/>
|
||||
<button onClick={sendMessage}>发送</button>
|
||||
</section>
|
||||
|
||||
<ul className="messages">
|
||||
{messages.map((m) => (
|
||||
<li key={m.messageId}>
|
||||
<span>#{m.seq}</span> <span>{m.content}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppLayout />}>
|
||||
<Route index element={<Navigate to="/workspace" replace />} />
|
||||
<Route path="workspace" element={<WorkspacePage />} />
|
||||
<Route path="chat" element={<ChatPage />} />
|
||||
<Route path="login" element={<LoginPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/workspace" replace />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
Reference in New Issue
Block a user