Compare commits

...

10 Commits

Author SHA1 Message Date
semantic-release-bot
aa6cea07df chore(release): 4.14.6 [skip ci]
## [4.14.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.5...v4.14.6) (2025-10-19)

### Bug Fixes

* replace native alerts with inline confirmations in ModerationPanel ([ebe123e](ebe123ed7e))

### Documentation

* add UI style guide documenting no native alerts rule ([9afd3a7](9afd3a7e92))
2025-10-19 17:08:16 +00:00
Thomas Hallock
ebe123ed7e fix: replace native alerts with inline confirmations in ModerationPanel
Removed native browser confirm() dialogs and replaced with React state-based inline confirmations:
- Removed confirm() from handleKick (kicks happen immediately)
- Removed confirm() from handleTransferOwnership
- Added confirmingTransferOwnership state variable
- Added inline confirmation UI with Cancel/Confirm buttons
- Follows pattern documented in UI_STYLE_GUIDE.md

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:07:18 -05:00
Thomas Hallock
9afd3a7e92 docs: add UI style guide documenting no native alerts rule
Documented the project's UI pattern requirements:
- Never use native browser dialogs (alert/confirm/prompt)
- Always use inline React-based confirmations
- Included pattern examples and migration checklist
- Referenced ModerationPanel.tsx for real examples

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:07:18 -05:00
semantic-release-bot
efb9c37380 chore(release): 4.14.5 [skip ci]
## [4.14.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.4...v4.14.5) (2025-10-19)

### Bug Fixes

* **rooms:** add real-time ownership transfer updates via WebSocket ([c00cfa3](c00cfa3de0))
2025-10-19 17:02:24 +00:00
Thomas Hallock
c00cfa3de0 fix(rooms): add real-time ownership transfer updates via WebSocket
When ownership is transferred via the moderation modal, both the old
and new host now see the change immediately without requiring a page
reload.

Added missing socket event handler for 'ownership-transferred' event:
- Server already broadcasts event with updated members (route.ts:82)
- Client now listens and updates React Query cache in real-time
- All components using useRoomData() automatically re-render
- Both sessions see host status changes instantly

Fixes issue where ownership transfer required manual page refresh
to see updated host permissions (game selection, moderation access).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:01:22 -05:00
semantic-release-bot
da53e084f0 chore(release): 4.14.4 [skip ci]
## [4.14.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.3...v4.14.4) (2025-10-19)

### Bug Fixes

* **arcade:** add host-only game selection with clear messaging ([22df1b0](22df1b0b66))
* **arcade:** add host-only game selection with clear messaging ([c0680ca](c0680cad0f))
2025-10-19 17:00:53 +00:00
Thomas Hallock
22df1b0b66 fix(arcade): add host-only game selection with clear messaging
Only room hosts can select games. Added clear visual messaging:
- Host: "👑 You're the room host. Select a game to start playing."
- Non-host: " Waiting for [Host Name] to select a game..."
- Error: "⚠️ Only the room host can select a game. Ask [Host] to choose."

Changes:
- Detect host status via currentMember?.isCreator
- Disable game buttons for non-hosts (opacity 0.4, cursor not-allowed)
- Client-side permission check before API call
- Error messages auto-dismiss after 5 seconds
- Error handling in setRoomGame mutation callback

Fixes 403 errors when non-hosts attempt game selection.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 11:59:43 -05:00
Thomas Hallock
c0680cad0f fix(arcade): add host-only game selection with clear messaging
Non-host room members were getting 403 errors when trying to select games.
Added proper UI restrictions and messaging to clarify only the host can
select games.

**Changes**:

1. **Host Detection**: Check if current user is room creator
   - Find `currentMember` in `roomData.members`
   - Check `isCreator` flag

2. **Visual Restrictions**:
   - Game buttons disabled for non-hosts (opacity: 0.4, cursor: not-allowed)
   - No hover effects when disabled
   - Clear visual feedback

3. **Messaging**:
   - **Host**: "👑 You're the room host. Select a game to start playing."
   - **Non-host**: " Waiting for [Host Name] to select a game..."
   - **Error**: "⚠️ Only the room host can select a game. Ask [Host] to choose."

4. **Error Handling**:
   - Client-side check before API call
   - Server error caught and displayed with host name
   - Auto-dismiss after 5 seconds

**UX Flow**:
- Non-hosts see disabled games with clear "waiting for host" message
- If they somehow click, they get clear error message
- Host sees active games with confirmation they can select

Prevents confusing 403 errors and clarifies room permissions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 11:59:43 -05:00
semantic-release-bot
0fef1dc9db chore(release): 4.14.3 [skip ci]
## [4.14.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.2...v4.14.3) (2025-10-19)

### Bug Fixes

* **docker:** add qpdf for PDF linearization and validation ([c92ff39](c92ff3971c))
2025-10-19 16:56:12 +00:00
Thomas Hallock
c92ff3971c fix(docker): add qpdf for PDF linearization and validation
The Python flashcard generator requires qpdf for PDF processing.
Without it, the script exits with code 1 even though it prints
a warning saying it will skip linearization.

Added qpdf to Alpine packages to fix PDF generation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 11:55:02 -05:00
9 changed files with 402 additions and 77 deletions

View File

@@ -1,3 +1,37 @@
## [4.14.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.5...v4.14.6) (2025-10-19)
### Bug Fixes
* replace native alerts with inline confirmations in ModerationPanel ([ebe123e](https://github.com/antialias/soroban-abacus-flashcards/commit/ebe123ed7edf24fbc7b8765ed709455a8513d6d5))
### Documentation
* add UI style guide documenting no native alerts rule ([9afd3a7](https://github.com/antialias/soroban-abacus-flashcards/commit/9afd3a7e925fddb76fa587747881b61f7cb077a5))
## [4.14.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.4...v4.14.5) (2025-10-19)
### Bug Fixes
* **rooms:** add real-time ownership transfer updates via WebSocket ([c00cfa3](https://github.com/antialias/soroban-abacus-flashcards/commit/c00cfa3de011720f3399fa340182b347f7e0d456))
## [4.14.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.3...v4.14.4) (2025-10-19)
### Bug Fixes
* **arcade:** add host-only game selection with clear messaging ([22df1b0](https://github.com/antialias/soroban-abacus-flashcards/commit/22df1b0b661efe69fac1a6bd716531c904757412))
* **arcade:** add host-only game selection with clear messaging ([c0680ca](https://github.com/antialias/soroban-abacus-flashcards/commit/c0680cad0fa26af0933e93a06c50317bf443cc7d))
## [4.14.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.2...v4.14.3) (2025-10-19)
### Bug Fixes
* **docker:** add qpdf for PDF linearization and validation ([c92ff39](https://github.com/antialias/soroban-abacus-flashcards/commit/c92ff3971c853e4e55ccd632ff3ee292fcce8315))
## [4.14.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.1...v4.14.2) (2025-10-19)

View File

@@ -48,8 +48,8 @@ RUN turbo build --filter=@soroban/web
FROM node:18-alpine AS runner
WORKDIR /app
# Install Python, pip, build tools for better-sqlite3, and Typst (needed at runtime)
RUN apk add --no-cache python3 py3-pip py3-setuptools make g++ typst
# Install Python, pip, build tools for better-sqlite3, Typst, and qpdf (needed at runtime)
RUN apk add --no-cache python3 py3-pip py3-setuptools make g++ typst qpdf
# Create non-root user
RUN addgroup --system --gid 1001 nodejs

View File

@@ -0,0 +1,94 @@
# UI Style Guide
## Confirmations and Dialogs
**NEVER use native browser dialogs:**
-`alert()`
-`confirm()`
-`prompt()`
**ALWAYS use inline React-based confirmations:**
- Show confirmation UI in-place using React state
- Provide Cancel and Confirm buttons
- Use descriptive warning messages with appropriate emoji (⚠️)
- Follow the Panda CSS styling system
- Match the visual style of the surrounding UI
### Pattern: Inline Confirmation
```typescript
const [confirming, setConfirming] = useState(false)
{!confirming ? (
<button onClick={() => setConfirming(true)}>
Delete Item
</button>
) : (
<div>
<div style={{ /* warning styling */ }}>
Are you sure you want to delete this item?
</div>
<div style={{ /* description styling */ }}>
This action cannot be undone.
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => setConfirming(false)}>
Cancel
</button>
<button onClick={handleDelete}>
Confirm Delete
</button>
</div>
</div>
)}
```
### Real Examples
See `/src/components/nav/ModerationPanel.tsx` for production examples:
- Transfer ownership confirmation (lines 1793-1929)
- Unban user confirmation (shows inline warning with Cancel/Confirm)
### Why This Pattern?
1. **Consistency**: Native dialogs look different across browsers and platforms
2. **Control**: We can style, position, and enhance confirmations to match our design
3. **Accessibility**: We can add proper ARIA attributes and keyboard navigation
4. **UX**: Users stay in context rather than being interrupted by modal dialogs
5. **Testing**: Inline confirmations are easier to test than native browser dialogs
### Migration Checklist
When replacing native dialogs:
- [ ] Add state variable for confirmation (e.g., `const [confirming, setConfirming] = useState(false)`)
- [ ] Remove the `confirm()` or `alert()` call from the handler
- [ ] Replace the original UI with conditional rendering
- [ ] Show initial state with primary action button
- [ ] Show confirmation state with warning message + Cancel/Confirm buttons
- [ ] Ensure Cancel button resets state: `onClick={() => setConfirming(false)}`
- [ ] Ensure Confirm button performs action and resets state
- [ ] Add loading states if the action is async
- [ ] Style to match surrounding UI using Panda CSS
## Styling System
This project uses **Panda CSS**, not Tailwind CSS.
- ❌ Never use Tailwind utility classes (e.g., `className="bg-blue-500"`)
- ✅ Always use Panda CSS `css()` function
- ✅ Use Panda's token system (defined in `panda.config.ts`)
See `.claude/CLAUDE.md` for complete Panda CSS documentation.
## Emoji Usage
Emojis are used liberally throughout the UI for visual communication:
- 👑 Host/owner status
- ⏳ Waiting states
- ⚠️ Warnings and confirmations
- ✅ Success states
- ❌ Error states
- 👀 Spectating mode
- 🎮 Gaming context
Use emojis to enhance clarity, not replace text.

View File

@@ -101,7 +101,8 @@
"WebFetch(domain:abaci.one)",
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run ID: \\(.databaseId)\"\"')",
"Bash(node -e:*)",
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run \\(.databaseId)\"\"')"
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run \\(.databaseId)\"\"')",
"Bash(do ssh nas.home.network '/usr/local/bin/docker inspect soroban-abacus-flashcards --format=\"\"{{index .Config.Labels \\\"\"org.opencontainers.image.revision\\\"\"}}\"\"')"
],
"deny": [],
"ask": []

View File

@@ -24,7 +24,8 @@ function getBuildInfo() {
const gitCommitShort = process.env.GIT_COMMIT_SHORT || exec('git rev-parse --short HEAD')
const gitBranch = process.env.GIT_BRANCH || exec('git rev-parse --abbrev-ref HEAD')
const gitTag = process.env.GIT_TAG || exec('git describe --tags --exact-match 2>/dev/null')
const gitDirty = process.env.GIT_DIRTY === 'true' || exec('git diff --quiet || echo "dirty"') === 'dirty'
const gitDirty =
process.env.GIT_DIRTY === 'true' || exec('git diff --quiet || echo "dirty"') === 'dirty'
const packageJson = require('../package.json')

View File

@@ -1,7 +1,9 @@
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { useRoomData, useSetRoomGame } from '@/hooks/useRoomData'
import { useViewerId } from '@/hooks/useViewerId'
import { GAMES_CONFIG } from '@/components/GameSelector'
import type { GameType } from '@/components/GameSelector'
import { PageWithNav } from '@/components/PageWithNav'
@@ -23,7 +25,9 @@ import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
const { data: viewerId } = useViewerId()
const { mutate: setRoomGame } = useSetRoomGame()
const [permissionError, setPermissionError] = useState<string | null>(null)
// Show loading state
if (isLoading) {
@@ -74,9 +78,27 @@ export default function RoomPage() {
// Show game selection if no game is set
if (!roomData.gameName) {
// Determine if current user is the host
const currentMember = roomData.members.find((m) => m.userId === viewerId)
const isHost = currentMember?.isCreator === true
const hostMember = roomData.members.find((m) => m.isCreator)
const handleGameSelect = (gameType: GameType) => {
console.log('[RoomPage] handleGameSelect called with gameType:', gameType)
// Check if user is host before allowing selection
if (!isHost) {
setPermissionError(
`Only the room host can select a game. Ask ${hostMember?.displayName || 'the host'} to choose.`
)
// Clear error after 5 seconds
setTimeout(() => setPermissionError(null), 5000)
return
}
// Clear any previous errors
setPermissionError(null)
// All games are now in the registry
if (hasGame(gameType)) {
const gameDef = getGame(gameType)
@@ -86,10 +108,21 @@ export default function RoomPage() {
}
console.log('[RoomPage] Selecting registry game:', gameType)
setRoomGame({
roomId: roomData.id,
gameName: gameType,
})
setRoomGame(
{
roomId: roomData.id,
gameName: gameType,
},
{
onError: (error: any) => {
console.error('[RoomPage] Failed to set game:', error)
setPermissionError(
error.message || 'Failed to select game. Only the host can change games.'
)
setTimeout(() => setPermissionError(null), 5000)
},
}
)
return
}
@@ -119,13 +152,70 @@ export default function RoomPage() {
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '8',
mb: '4',
textAlign: 'center',
})}
>
Choose a Game
</h1>
{/* Host info and permission messaging */}
<div
className={css({
maxWidth: '800px',
width: '100%',
mb: '6',
})}
>
{isHost ? (
<div
className={css({
background: 'rgba(34, 197, 94, 0.1)',
border: '1px solid rgba(34, 197, 94, 0.3)',
borderRadius: '8px',
padding: '12px 16px',
color: '#86efac',
fontSize: 'sm',
textAlign: 'center',
})}
>
👑 You're the room host. Select a game to start playing.
</div>
) : (
<div
className={css({
background: 'rgba(234, 179, 8, 0.1)',
border: '1px solid rgba(234, 179, 8, 0.3)',
borderRadius: '8px',
padding: '12px 16px',
color: '#fde047',
fontSize: 'sm',
textAlign: 'center',
})}
>
⏳ Waiting for {hostMember?.displayName || 'the host'} to select a game...
</div>
)}
{/* Permission error message */}
{permissionError && (
<div
className={css({
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.3)',
borderRadius: '8px',
padding: '12px 16px',
color: '#fca5a5',
fontSize: 'sm',
textAlign: 'center',
mt: '3',
})}
>
⚠️ {permissionError}
</div>
)}
</div>
<div
className={css({
display: 'grid',
@@ -138,21 +228,22 @@ export default function RoomPage() {
{/* Legacy games */}
{Object.entries(GAMES_CONFIG).map(([gameType, config]: [string, any]) => {
const isAvailable = !('available' in config) || config.available !== false
const isDisabled = !isHost || !isAvailable
return (
<button
key={gameType}
onClick={() => handleGameSelect(gameType as GameType)}
disabled={!isAvailable}
disabled={isDisabled}
className={css({
background: config.gradient,
border: '2px solid',
borderColor: config.borderColor || 'blue.200',
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.4 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
_hover: isDisabled
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
@@ -193,21 +284,22 @@ export default function RoomPage() {
{/* Registry games */}
{getAllGames().map((gameDef) => {
const isAvailable = gameDef.manifest.available
const isDisabled = !isHost || !isAvailable
return (
<button
key={gameDef.manifest.name}
onClick={() => handleGameSelect(gameDef.manifest.name)}
disabled={!isAvailable}
disabled={isDisabled}
className={css({
background: gameDef.manifest.gradient,
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.4 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
_hover: isDisabled
? {}
: {
transform: 'translateY(-4px) scale(1.02)',

View File

@@ -114,6 +114,9 @@ export function ModerationPanel({
null
)
// Transfer ownership confirmation state
const [confirmingTransferOwnership, setConfirmingTransferOwnership] = useState(false)
// Auto-switch to Members tab when focusedUserId is provided
useEffect(() => {
if (isOpen && focusedUserId) {
@@ -171,8 +174,6 @@ export function ModerationPanel({
}, [isOpen, roomId, members])
const handleKick = async (userId: string) => {
if (!confirm('Kick this player from the room?')) return
setActionLoading(`kick-${userId}`)
try {
const res = await fetch(`/api/arcade/rooms/${roomId}/kick`, {
@@ -414,10 +415,9 @@ export function ModerationPanel({
const newOwner = members.find((m) => m.userId === selectedNewOwner)
if (!newOwner) return
if (!confirm(`Transfer ownership to ${newOwner.displayName}? You will no longer be the host.`))
return
setConfirmingTransferOwnership(false)
setActionLoading('transfer-ownership')
try {
const res = await fetch(`/api/arcade/rooms/${roomId}/transfer-ownership`, {
method: 'POST',
@@ -436,6 +436,7 @@ export function ModerationPanel({
showError(err instanceof Error ? err.message : 'Failed to transfer ownership')
} finally {
setActionLoading(null)
setSelectedNewOwner('') // Reset selection
}
}
@@ -1789,61 +1790,143 @@ export function ModerationPanel({
Transfer host privileges to another member. You will no longer be the host.
</p>
<select
value={selectedNewOwner}
onChange={(e) => setSelectedNewOwner(e.target.value)}
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',
marginBottom: '12px',
cursor: 'pointer',
}}
>
<option value="">Select new owner...</option>
{otherMembers.map((member) => (
<option key={member.userId} value={member.userId}>
{member.displayName}
{member.isOnline ? ' (Online)' : ' (Offline)'}
</option>
))}
</select>
{!confirmingTransferOwnership ? (
<>
<select
value={selectedNewOwner}
onChange={(e) => setSelectedNewOwner(e.target.value)}
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',
marginBottom: '12px',
cursor: 'pointer',
}}
>
<option value="">Select new owner...</option>
{otherMembers.map((member) => (
<option key={member.userId} value={member.userId}>
{member.displayName}
{member.isOnline ? ' (Online)' : ' (Offline)'}
</option>
))}
</select>
<button
type="button"
onClick={handleTransferOwnership}
disabled={!selectedNewOwner || actionLoading === 'transfer-ownership'}
style={{
width: '100%',
padding: '10px',
background:
!selectedNewOwner || actionLoading === 'transfer-ownership'
? 'rgba(75, 85, 99, 0.3)'
: 'linear-gradient(135deg, rgba(251, 146, 60, 0.8), rgba(249, 115, 22, 0.8))',
color: 'white',
border:
!selectedNewOwner || actionLoading === 'transfer-ownership'
? '1px solid rgba(75, 85, 99, 0.5)'
: '1px solid rgba(251, 146, 60, 0.6)',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600',
cursor:
!selectedNewOwner || actionLoading === 'transfer-ownership'
? 'not-allowed'
: 'pointer',
opacity:
!selectedNewOwner || actionLoading === 'transfer-ownership' ? 0.5 : 1,
}}
>
{actionLoading === 'transfer-ownership'
? 'Transferring...'
: 'Transfer Ownership'}
</button>
<button
type="button"
onClick={() => setConfirmingTransferOwnership(true)}
disabled={!selectedNewOwner}
style={{
width: '100%',
padding: '10px',
background: !selectedNewOwner
? 'rgba(75, 85, 99, 0.3)'
: 'linear-gradient(135deg, rgba(251, 146, 60, 0.8), rgba(249, 115, 22, 0.8))',
color: 'white',
border: !selectedNewOwner
? '1px solid rgba(75, 85, 99, 0.5)'
: '1px solid rgba(251, 146, 60, 0.6)',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600',
cursor: !selectedNewOwner ? 'not-allowed' : 'pointer',
opacity: !selectedNewOwner ? 0.5 : 1,
}}
>
Transfer Ownership
</button>
</>
) : (
<div>
<div
style={{
fontSize: '13px',
fontWeight: '600',
color: 'rgba(251, 191, 36, 1)',
marginBottom: '8px',
}}
>
Confirm Transfer to{' '}
{members.find((m) => m.userId === selectedNewOwner)?.displayName}?
</div>
<div
style={{
fontSize: '12px',
color: 'rgba(209, 213, 219, 0.8)',
marginBottom: '12px',
}}
>
You will no longer be the host and will lose moderation privileges. This
cannot be undone.
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
type="button"
onClick={() => setConfirmingTransferOwnership(false)}
disabled={actionLoading === 'transfer-ownership'}
style={{
flex: 1,
padding: '10px',
background: 'rgba(75, 85, 99, 0.3)',
color: 'rgba(209, 213, 219, 1)',
border: '1px solid rgba(75, 85, 99, 0.5)',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600',
cursor:
actionLoading === 'transfer-ownership' ? 'not-allowed' : 'pointer',
opacity: actionLoading === 'transfer-ownership' ? 0.5 : 1,
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
if (actionLoading !== 'transfer-ownership') {
e.currentTarget.style.background = 'rgba(75, 85, 99, 0.4)'
}
}}
onMouseLeave={(e) => {
if (actionLoading !== 'transfer-ownership') {
e.currentTarget.style.background = 'rgba(75, 85, 99, 0.3)'
}
}}
>
Cancel
</button>
<button
type="button"
onClick={handleTransferOwnership}
disabled={actionLoading === 'transfer-ownership'}
style={{
flex: 1,
padding: '10px',
background:
actionLoading === 'transfer-ownership'
? 'rgba(75, 85, 99, 0.3)'
: 'linear-gradient(135deg, rgba(251, 146, 60, 0.8), rgba(249, 115, 22, 0.8))',
color: 'white',
border:
actionLoading === 'transfer-ownership'
? '1px solid rgba(75, 85, 99, 0.5)'
: '1px solid rgba(251, 146, 60, 0.6)',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600',
cursor:
actionLoading === 'transfer-ownership' ? 'not-allowed' : 'pointer',
opacity: actionLoading === 'transfer-ownership' ? 0.5 : 1,
transition: 'all 0.2s ease',
}}
>
{actionLoading === 'transfer-ownership'
? 'Transferring...'
: 'Confirm Transfer'}
</button>
</div>
</div>
)}
</div>
</div>
</div>

View File

@@ -464,6 +464,24 @@ export function useRoomData() {
}
}
const handleOwnershipTransferred = (data: {
roomId: string
oldOwnerId: string
newOwnerId: string
newOwnerName: string
members: RoomMember[]
}) => {
if (data.roomId === roomData?.id) {
queryClient.setQueryData<RoomData | null>(roomKeys.current(), (prev) => {
if (!prev) return null
return {
...prev,
members: data.members,
}
})
}
}
socket.on('room-joined', handleRoomJoined)
socket.on('member-joined', handleMemberJoined)
socket.on('member-left', handleMemberLeft)
@@ -474,6 +492,7 @@ export function useRoomData() {
socket.on('room-invitation-received', handleInvitationReceived)
socket.on('join-request-submitted', handleJoinRequestSubmitted)
socket.on('room-game-changed', handleRoomGameChanged)
socket.on('ownership-transferred', handleOwnershipTransferred)
return () => {
socket.off('room-joined', handleRoomJoined)
@@ -486,6 +505,7 @@ export function useRoomData() {
socket.off('room-invitation-received', handleInvitationReceived)
socket.off('join-request-submitted', handleJoinRequestSubmitted)
socket.off('room-game-changed', handleRoomGameChanged)
socket.off('ownership-transferred', handleOwnershipTransferred)
}
}, [socket, roomData?.id, queryClient])

View File

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