feat(frontend): add guild join flow, members panel, and channel-create member modal
This commit is contained in:
@@ -2,6 +2,7 @@ import axios from 'axios'
|
||||
import { io, type Socket } from 'socket.io-client'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useAuth } from '../auth/auth-context'
|
||||
import { guildMembersCenter, joinGuildCenter } from '../lib/center-auth-client'
|
||||
|
||||
type MessageItem = {
|
||||
messageId: string
|
||||
@@ -11,22 +12,24 @@ type MessageItem = {
|
||||
}
|
||||
|
||||
type GuildChannel = { id: string; name: string; guildId?: string }
|
||||
type GuildRecord = { id: string; name: string }
|
||||
type GuildMember = { id: string; userId: string; guildId: string; status: string }
|
||||
type MemberItem = { userId: string; email: string; status: string }
|
||||
|
||||
export default function ChatPage() {
|
||||
const { session, logout } = useAuth()
|
||||
const { session, logout, ensureFreshToken, refreshGuilds } = useAuth()
|
||||
const [selectedGuildId, setSelectedGuildId] = useState('')
|
||||
const [selectedChannelId, setSelectedChannelId] = useState('')
|
||||
const [channels, setChannels] = useState<GuildChannel[]>([])
|
||||
const [guildDbId, setGuildDbId] = useState('')
|
||||
const [members, setMembers] = useState<GuildMember[]>([])
|
||||
const [showGuilds, setShowGuilds] = useState(true)
|
||||
const [showChannels, setShowChannels] = useState(true)
|
||||
const [showMembers, setShowMembers] = useState(true)
|
||||
const [members, setMembers] = useState<MemberItem[]>([])
|
||||
const [messages, setMessages] = useState<MessageItem[]>([])
|
||||
const [content, setContent] = useState('')
|
||||
const [newChannelName, setNewChannelName] = useState('')
|
||||
const [joinGuildNodeId, setJoinGuildNodeId] = useState('')
|
||||
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([])
|
||||
const [showCreateChannelModal, setShowCreateChannelModal] = useState(false)
|
||||
const [showGuilds, setShowGuilds] = useState(true)
|
||||
const [showChannels, setShowChannels] = useState(true)
|
||||
const [showMembers, setShowMembers] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
@@ -64,9 +67,7 @@ export default function ChatPage() {
|
||||
if (!guild || !guildToken) return
|
||||
setError('')
|
||||
try {
|
||||
const res = await guildApi().get('/channels', {
|
||||
params: guildDbId ? { guildId: guildDbId } : undefined,
|
||||
})
|
||||
const res = await guildApi().get('/channels', { params: guildDbId ? { guildId: guildDbId } : undefined })
|
||||
const list = Array.isArray(res.data) ? (res.data as GuildChannel[]) : []
|
||||
setChannels(list)
|
||||
if (!guildDbId && list[0]?.guildId) setGuildDbId(list[0].guildId)
|
||||
@@ -77,20 +78,14 @@ export default function ChatPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGuildMetaAndMembers() {
|
||||
if (!guild || !guildToken) return
|
||||
async function loadMembers() {
|
||||
if (!session || !selectedGuildId) return
|
||||
setError('')
|
||||
try {
|
||||
const guildRes = await guildApi().get('/guilds')
|
||||
const guildList = Array.isArray(guildRes.data) ? (guildRes.data as GuildRecord[]) : []
|
||||
const nextGuildDbId = guildList[0]?.id ?? ''
|
||||
setGuildDbId(nextGuildDbId)
|
||||
|
||||
const memberRes = await guildApi().get('/members', {
|
||||
params: nextGuildDbId ? { guildId: nextGuildDbId } : undefined,
|
||||
})
|
||||
const memberList = Array.isArray(memberRes.data) ? (memberRes.data as GuildMember[]) : []
|
||||
setMembers(memberList)
|
||||
const token = await ensureFreshToken()
|
||||
if (!token) return
|
||||
const list = await guildMembersCenter(session.centerApiBase, token, selectedGuildId)
|
||||
setMembers(list)
|
||||
} catch {
|
||||
setError('Failed to load guild members')
|
||||
setMembers([])
|
||||
@@ -128,6 +123,24 @@ export default function ChatPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function addGuild() {
|
||||
if (!session || !joinGuildNodeId.trim()) return
|
||||
setError('')
|
||||
try {
|
||||
const token = await ensureFreshToken()
|
||||
if (!token) return
|
||||
await joinGuildCenter(session.centerApiBase, token, joinGuildNodeId.trim())
|
||||
await refreshGuilds()
|
||||
setJoinGuildNodeId('')
|
||||
} catch {
|
||||
setError('Failed to add guild')
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMember(userId: string) {
|
||||
setSelectedMemberIds((prev) => (prev.includes(userId) ? prev.filter((x) => x !== userId) : [...prev, userId]))
|
||||
}
|
||||
|
||||
async function createChannel() {
|
||||
if (!guild || !guildToken || !newChannelName.trim()) return
|
||||
if (!guildDbId) {
|
||||
@@ -136,9 +149,16 @@ export default function ChatPage() {
|
||||
}
|
||||
setError('')
|
||||
try {
|
||||
const res = await guildApi().post('/channels', { name: newChannelName.trim(), guildId: guildDbId })
|
||||
const payload = {
|
||||
name: newChannelName.trim(),
|
||||
guildId: guildDbId,
|
||||
memberUserIds: selectedMemberIds,
|
||||
}
|
||||
const res = await guildApi().post('/channels', payload)
|
||||
const createdId = res.data?.id as string | undefined
|
||||
setNewChannelName('')
|
||||
setSelectedMemberIds([])
|
||||
setShowCreateChannelModal(false)
|
||||
await loadChannels()
|
||||
if (createdId) setSelectedChannelId(createdId)
|
||||
} catch {
|
||||
@@ -147,7 +167,8 @@ export default function ChatPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadGuildMetaAndMembers()
|
||||
void loadMembers()
|
||||
setSelectedMemberIds([])
|
||||
}, [selectedGuildId])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -186,6 +207,10 @@ export default function ChatPage() {
|
||||
<div className={`chat-top-grid ${!showGuilds ? 'hide-v1' : ''} ${!showChannels ? 'hide-v2' : ''} ${!showMembers ? 'hide-v4' : ''}`}>
|
||||
<div className="panel col-list col-v1">
|
||||
<h3>Guilds</h3>
|
||||
<div className="row-wrap" style={{ marginBottom: 8 }}>
|
||||
<input className="input" value={joinGuildNodeId} onChange={(e) => setJoinGuildNodeId(e.target.value)} placeholder="Guild nodeId" />
|
||||
<button className="btn btn-secondary" onClick={addGuild}>Add guild</button>
|
||||
</div>
|
||||
<ul className="list-reset">
|
||||
{guilds.map((g) => (
|
||||
<li key={g.nodeId}>
|
||||
@@ -200,13 +225,7 @@ export default function ChatPage() {
|
||||
<div className="panel col-list col-v2">
|
||||
<h3>Channels</h3>
|
||||
<div className="row-wrap" style={{ marginBottom: 8 }}>
|
||||
<input
|
||||
className="input"
|
||||
value={newChannelName}
|
||||
onChange={(e) => setNewChannelName(e.target.value)}
|
||||
placeholder="New channel name"
|
||||
/>
|
||||
<button className="btn btn-secondary" onClick={createChannel}>Create channel</button>
|
||||
<button className="btn btn-secondary" onClick={() => setShowCreateChannelModal(true)}>Create channel</button>
|
||||
</div>
|
||||
<ul className="list-reset">
|
||||
{channels.map((c) => (
|
||||
@@ -244,9 +263,7 @@ export default function ChatPage() {
|
||||
<h3>Members</h3>
|
||||
<ul className="list-reset">
|
||||
{members.map((m) => (
|
||||
<li key={m.id}>
|
||||
<span className="muted">{m.userId}</span>
|
||||
</li>
|
||||
<li key={m.userId}><span className="muted">{m.email}</span></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -259,6 +276,28 @@ export default function ChatPage() {
|
||||
<button className="btn btn-secondary" onClick={() => void logout()}>Logout</button>
|
||||
<button className="btn btn-secondary" type="button">Settings</button>
|
||||
</div>
|
||||
|
||||
{showCreateChannelModal ? (
|
||||
<div className="modal-backdrop" onClick={() => setShowCreateChannelModal(false)}>
|
||||
<div className="modal-card" onClick={(e) => e.stopPropagation()}>
|
||||
<h3>Create channel</h3>
|
||||
<input className="input" value={newChannelName} onChange={(e) => setNewChannelName(e.target.value)} placeholder="Channel name" />
|
||||
<p className="muted" style={{ marginTop: 8 }}>Select members to include</p>
|
||||
<div className="modal-list">
|
||||
{members.map((m) => (
|
||||
<label key={m.userId} className="row-wrap" style={{ alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={selectedMemberIds.includes(m.userId)} onChange={() => toggleMember(m.userId)} />
|
||||
<span>{m.email}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="row-wrap" style={{ marginTop: 12 }}>
|
||||
<button className="btn" onClick={createChannel}>Create</button>
|
||||
<button className="btn btn-secondary" onClick={() => setShowCreateChannelModal(false)}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user