Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c6eb01f1e | ||
|
|
7d08fdd906 | ||
|
|
0d4f400dca | ||
|
|
396b6c07c7 | ||
|
|
35b4a72c8b | ||
|
|
ba916e0f65 | ||
|
|
e5d0672059 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,3 +1,29 @@
|
||||
## [3.6.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.6.0...v3.6.1) (2025-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* join user socket channel to receive approval notifications ([7d08fdd](https://github.com/antialias/soroban-abacus-flashcards/commit/7d08fdd90643920857eda09998ac01afbae74154))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* remove redundant polling from approval notifications ([0d4f400](https://github.com/antialias/soroban-abacus-flashcards/commit/0d4f400dca02ad9497522c24fded8b6d07d85fd2))
|
||||
|
||||
## [3.6.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.5.0...v3.6.0) (2025-10-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add socket listener and polling for approval notifications ([35b4a72](https://github.com/antialias/soroban-abacus-flashcards/commit/35b4a72c8b2f80a74b5d2fe02b048d4ec4d1d6f2))
|
||||
|
||||
## [3.5.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.4.0...v3.5.0) (2025-10-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* replace access mode dropdown with visual button grid ([e5d0672](https://github.com/antialias/soroban-abacus-flashcards/commit/e5d067205989d7c3105998dcd7d67fd0408f332c))
|
||||
|
||||
## [3.4.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.3.1...v3.4.0) (2025-10-14)
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { io } from 'socket.io-client'
|
||||
import { useGetRoomByCode, useJoinRoom, useRoomData } from '@/hooks/useRoomData'
|
||||
import { getRoomDisplayWithEmoji } from '@/utils/room-display'
|
||||
|
||||
@@ -351,6 +352,60 @@ export default function JoinRoomPage({ params }: { params: { code: string } }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Socket listener for approval notifications
|
||||
useEffect(() => {
|
||||
if (!approvalRequested || !targetRoomData) return
|
||||
|
||||
console.log('[Join Page] Setting up approval listener for room:', targetRoomData.id)
|
||||
|
||||
let socket: ReturnType<typeof io> | null = null
|
||||
|
||||
// Fetch viewer ID and set up socket
|
||||
const setupSocket = async () => {
|
||||
try {
|
||||
// Get current user's viewer ID
|
||||
const res = await fetch('/api/viewer')
|
||||
if (!res.ok) {
|
||||
console.error('[Join Page] Failed to get viewer ID')
|
||||
return
|
||||
}
|
||||
|
||||
const { viewerId } = await res.json()
|
||||
console.log('[Join Page] Got viewer ID:', viewerId)
|
||||
|
||||
// Connect socket
|
||||
socket = io({ path: '/api/socket' })
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('[Join Page] Socket connected, joining user channel')
|
||||
// Join user-specific channel to receive moderation events
|
||||
socket?.emit('join-user-channel', { userId: viewerId })
|
||||
})
|
||||
|
||||
socket.on('join-request-approved', (data: { roomId: string; requestId: string }) => {
|
||||
console.log('[Join Page] Request approved via socket!', data)
|
||||
if (data.roomId === targetRoomData.id) {
|
||||
console.log('[Join Page] Joining room automatically...')
|
||||
handleJoin(targetRoomData.id)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('[Join Page] Socket connection error:', error)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Join Page] Error setting up socket:', error)
|
||||
}
|
||||
}
|
||||
|
||||
setupSocket()
|
||||
|
||||
return () => {
|
||||
console.log('[Join Page] Cleaning up approval listener')
|
||||
socket?.disconnect()
|
||||
}
|
||||
}, [approvalRequested, targetRoomData, handleJoin])
|
||||
|
||||
// Only show error page for non-password and non-approval errors
|
||||
if (error && !showPasswordPrompt && !showApprovalPrompt) {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { io } from 'socket.io-client'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
import type { schema } from '@/db'
|
||||
import { useRoomData } from '@/hooks/useRoomData'
|
||||
@@ -142,6 +143,67 @@ export function JoinRoomModal({ isOpen, onClose, onSuccess }: JoinRoomModalProps
|
||||
}
|
||||
}
|
||||
|
||||
// Socket listener for approval notifications
|
||||
useEffect(() => {
|
||||
if (!approvalRequested || !roomInfo) return
|
||||
|
||||
console.log('[JoinRoomModal] Setting up approval listener for room:', roomInfo.id)
|
||||
|
||||
let socket: ReturnType<typeof io> | null = null
|
||||
|
||||
// Fetch viewer ID and set up socket
|
||||
const setupSocket = async () => {
|
||||
try {
|
||||
// Get current user's viewer ID
|
||||
const res = await fetch('/api/viewer')
|
||||
if (!res.ok) {
|
||||
console.error('[JoinRoomModal] Failed to get viewer ID')
|
||||
return
|
||||
}
|
||||
|
||||
const { viewerId } = await res.json()
|
||||
console.log('[JoinRoomModal] Got viewer ID:', viewerId)
|
||||
|
||||
// Connect socket
|
||||
socket = io({ path: '/api/socket' })
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('[JoinRoomModal] Socket connected, joining user channel')
|
||||
// Join user-specific channel to receive moderation events
|
||||
socket?.emit('join-user-channel', { userId: viewerId })
|
||||
})
|
||||
|
||||
socket.on('join-request-approved', async (data: { roomId: string; requestId: string }) => {
|
||||
console.log('[JoinRoomModal] Request approved via socket!', data)
|
||||
if (data.roomId === roomInfo.id) {
|
||||
console.log('[JoinRoomModal] Joining room automatically...')
|
||||
try {
|
||||
await joinRoom(roomInfo.id)
|
||||
handleClose()
|
||||
onSuccess?.()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to join room')
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('[JoinRoomModal] Socket connection error:', error)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[JoinRoomModal] Error setting up socket:', error)
|
||||
}
|
||||
}
|
||||
|
||||
setupSocket()
|
||||
|
||||
return () => {
|
||||
console.log('[JoinRoomModal] Cleaning up approval listener')
|
||||
socket?.disconnect()
|
||||
}
|
||||
}, [approvalRequested, roomInfo, joinRoom, handleClose, onSuccess])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose}>
|
||||
<div
|
||||
|
||||
@@ -1333,31 +1333,81 @@ export function ModerationPanel({
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<select
|
||||
value={accessMode}
|
||||
onChange={(e) => {
|
||||
setAccessMode(e.target.value)
|
||||
setShowPasswordInput(e.target.value === 'password')
|
||||
}}
|
||||
{/* Access mode button grid */}
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
border: '1px solid rgba(75, 85, 99, 0.5)',
|
||||
borderRadius: '6px',
|
||||
color: 'rgba(209, 213, 219, 1)',
|
||||
fontSize: '14px',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '8px',
|
||||
marginBottom: '12px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<option value="open">🌐 Open - Anyone can join</option>
|
||||
<option value="password">🔑 Password Protected</option>
|
||||
<option value="approval-only">✋ Approval Required</option>
|
||||
<option value="restricted">🚫 Restricted - Invitation only</option>
|
||||
<option value="locked">🔒 Locked - No new members</option>
|
||||
<option value="retired">🏁 Retired - Room closed</option>
|
||||
</select>
|
||||
{[
|
||||
{ value: 'open', emoji: '🌐', label: 'Open', desc: 'Anyone' },
|
||||
{ value: 'password', emoji: '🔑', label: 'Password', desc: 'With key' },
|
||||
{ value: 'approval-only', emoji: '✋', label: 'Approval', desc: 'Request' },
|
||||
{
|
||||
value: 'restricted',
|
||||
emoji: '🚫',
|
||||
label: 'Restricted',
|
||||
desc: 'Invite only',
|
||||
},
|
||||
{ value: 'locked', emoji: '🔒', label: 'Locked', desc: 'No members' },
|
||||
{ value: 'retired', emoji: '🏁', label: 'Retired', desc: 'Closed' },
|
||||
].map((mode) => (
|
||||
<button
|
||||
key={mode.value}
|
||||
type="button"
|
||||
disabled={actionLoading === 'update-settings'}
|
||||
onClick={() => {
|
||||
setAccessMode(mode.value)
|
||||
setShowPasswordInput(mode.value === 'password')
|
||||
}}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
background:
|
||||
accessMode === mode.value
|
||||
? 'rgba(253, 186, 116, 0.15)'
|
||||
: 'rgba(255, 255, 255, 0.05)',
|
||||
border:
|
||||
accessMode === mode.value
|
||||
? '2px solid rgba(253, 186, 116, 0.6)'
|
||||
: '2px solid rgba(75, 85, 99, 0.5)',
|
||||
borderRadius: '8px',
|
||||
color:
|
||||
accessMode === mode.value
|
||||
? 'rgba(253, 186, 116, 1)'
|
||||
: 'rgba(209, 213, 219, 0.8)',
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
cursor: actionLoading === 'update-settings' ? 'not-allowed' : 'pointer',
|
||||
opacity: actionLoading === 'update-settings' ? 0.5 : 1,
|
||||
transition: 'all 0.2s ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (actionLoading !== 'update-settings' && accessMode !== mode.value) {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)'
|
||||
e.currentTarget.style.borderColor = 'rgba(253, 186, 116, 0.4)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (accessMode !== mode.value) {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'
|
||||
e.currentTarget.style.borderColor = 'rgba(75, 85, 99, 0.5)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '18px' }}>{mode.emoji}</span>
|
||||
<div style={{ textAlign: 'left', flex: 1, lineHeight: '1.2' }}>
|
||||
<div style={{ fontSize: '13px', fontWeight: '600' }}>{mode.label}</div>
|
||||
<div style={{ fontSize: '11px', opacity: 0.7 }}>{mode.desc}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Password input (conditional) */}
|
||||
{(accessMode === 'password' || showPasswordInput) && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "3.4.0",
|
||||
"version": "3.6.1",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user