Compare commits

...

7 Commits

Author SHA1 Message Date
semantic-release-bot
4c6eb01f1e chore(release): 3.6.1 [skip ci]
## [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](7d08fdd906))

### Code Refactoring

* remove redundant polling from approval notifications ([0d4f400](0d4f400dca))
2025-10-14 12:44:53 +00:00
Thomas Hallock
7d08fdd906 fix: join user socket channel to receive approval notifications
The socket wasn't receiving join-request-approved events because it hadn't
joined the user-specific channel. Now:

- Fetch viewer ID from /api/viewer endpoint
- Emit 'join-user-channel' with userId on socket connect
- Socket joins `user:${userId}` room to receive moderation events
- Approval notifications now trigger automatic room join

This completes the real-time approval notification flow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 07:43:49 -05:00
Thomas Hallock
0d4f400dca refactor: remove redundant polling from approval notifications
Remove polling interval that checked every 5 seconds for approval status.
The socket.io listener provides real-time notifications, making polling
unnecessary and wasteful.

Now relies solely on socket.io for instant approval notifications, which:
- Reduces network traffic
- Simplifies code
- Provides faster response time

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 07:40:02 -05:00
semantic-release-bot
396b6c07c7 chore(release): 3.6.0 [skip ci]
## [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](35b4a72c8b))
2025-10-14 12:38:33 +00:00
Thomas Hallock
35b4a72c8b feat: add socket listener and polling for approval notifications
When users request to join an approval-only room, they now receive real-time
notifications when their request is approved:

- Add socket.io-client listener for 'join-request-approved' events
- Implement polling fallback (every 5 seconds) to check approval status
- Automatically join room when approval is detected via socket or polling
- Apply to both share link page and JoinRoomModal

This completes the approval flow - users no longer need to reload the page
to see if their join request was approved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 07:37:28 -05:00
semantic-release-bot
ba916e0f65 chore(release): 3.5.0 [skip ci]
## [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](e5d0672059))
2025-10-14 12:31:26 +00:00
Thomas Hallock
e5d0672059 feat: replace access mode dropdown with visual button grid
Updated the ModerationPanel Settings tab to use a visual button grid
for access mode selection, matching the CreateRoomModal UX.

Changes:
- Replaced <select> dropdown with 3x2 grid of buttons
- Each button shows emoji + label + description
- Visual feedback for selected state and hover
- Includes all 6 access modes: open, password, approval-only,
  restricted, locked, retired
- Maintains same functionality with improved UX

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 07:30:26 -05:00
5 changed files with 216 additions and 23 deletions

View File

@@ -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)

View File

@@ -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 (

View File

@@ -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

View File

@@ -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) && (

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "3.4.0",
"version": "3.6.1",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [