feat(room-share): add QR code button for easy mobile joining

Add a QR code sharing option alongside existing copy buttons.
When clicked, opens a popover with:
- QR code encoding the room's share URL
- "Scan to Join" heading
- Clickable copy button for the URL

Uses qrcode.react library with Radix UI popover component.
Button styled with orange gradient to differentiate from existing
blue link and purple code copy buttons.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-10-23 10:25:23 -05:00
parent 5927f61c3c
commit 349290ac6a
3 changed files with 202 additions and 0 deletions

View File

@ -66,6 +66,7 @@
"next": "^14.2.32",
"next-auth": "5.0.0-beta.29",
"python-bridge": "^1.1.0",
"qrcode.react": "^4.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-resizable-layout": "^0.7.3",

View File

@ -0,0 +1,198 @@
import * as Popover from '@radix-ui/react-popover'
import type { CSSProperties } from 'react'
import { useState } from 'react'
import { QRCodeSVG } from 'qrcode.react'
import { useClipboard } from '@/hooks/useClipboard'
export interface QRCodeButtonProps {
/**
* The URL to encode in the QR code and display
*/
url: string
/**
* Optional custom styles for the trigger button
*/
style?: CSSProperties
}
/**
* Button that opens a popover with a QR code for the share link
* Includes the URL text with a copy button
*/
export function QRCodeButton({ url, style }: QRCodeButtonProps) {
const [open, setOpen] = useState(false)
const { copied, copy } = useClipboard()
const buttonStyles: CSSProperties = {
width: '100%',
cursor: 'pointer',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
marginBottom: '6px',
border: '2px solid rgba(251, 146, 60, 0.4)',
background: 'linear-gradient(135deg, rgba(251, 146, 60, 0.2), rgba(251, 146, 60, 0.3))',
borderRadius: '8px',
padding: '10px 16px',
fontSize: '13px',
fontWeight: '600',
color: 'rgba(253, 186, 116, 1)',
...style,
}
const hoverStyles: CSSProperties = {
background: 'linear-gradient(135deg, rgba(251, 146, 60, 0.3), rgba(251, 146, 60, 0.4))',
borderColor: 'rgba(251, 146, 60, 0.6)',
}
return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<button
type="button"
style={buttonStyles}
onMouseEnter={(e) => {
Object.assign(e.currentTarget.style, hoverStyles)
}}
onMouseLeave={(e) => {
Object.assign(e.currentTarget.style, buttonStyles)
}}
>
<span style={{ fontSize: '16px' }}>📱</span>
<span>QR Code</span>
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
align="center"
side="bottom"
sideOffset={8}
style={{
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
border: '2px solid rgba(251, 146, 60, 0.4)',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
zIndex: 1000,
maxWidth: '320px',
}}
>
{/* Title */}
<div
style={{
fontSize: '16px',
fontWeight: 'bold',
color: 'rgba(253, 186, 116, 1)',
marginBottom: '12px',
textAlign: 'center',
}}
>
Scan to Join
</div>
{/* QR Code */}
<div
style={{
background: 'white',
padding: '16px',
borderRadius: '8px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginBottom: '16px',
}}
>
<QRCodeSVG value={url} size={200} level="H" />
</div>
{/* URL with copy button */}
<div
style={{
marginBottom: '8px',
}}
>
<div
style={{
fontSize: '11px',
color: 'rgba(209, 213, 219, 0.7)',
marginBottom: '6px',
textAlign: 'center',
}}
>
Or copy link:
</div>
<button
type="button"
onClick={() => copy(url)}
style={{
width: '100%',
cursor: 'pointer',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
background: copied
? 'linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(34, 197, 94, 0.3))'
: 'linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.3))',
border: copied
? '2px solid rgba(34, 197, 94, 0.5)'
: '2px solid rgba(59, 130, 246, 0.4)',
borderRadius: '8px',
padding: '8px 12px',
fontSize: '11px',
fontWeight: '600',
color: copied ? 'rgba(134, 239, 172, 1)' : 'rgba(147, 197, 253, 1)',
}}
onMouseEnter={(e) => {
if (!copied) {
e.currentTarget.style.background =
'linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(59, 130, 246, 0.4))'
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.6)'
}
}}
onMouseLeave={(e) => {
if (!copied) {
e.currentTarget.style.background =
'linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.3))'
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.4)'
}
}}
>
{copied ? (
<>
<span style={{ fontSize: '12px' }}></span>
<span>Copied!</span>
</>
) : (
<>
<span style={{ fontSize: '12px' }}>🔗</span>
<span
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '200px',
}}
>
{url}
</span>
</>
)}
</button>
</div>
<Popover.Arrow
style={{
fill: 'rgba(251, 146, 60, 0.4)',
}}
/>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
)
}

View File

@ -1,4 +1,5 @@
import { CopyButton } from '@/components/common/CopyButton'
import { QRCodeButton } from '@/components/common/QRCodeButton'
export interface RoomShareButtonsProps {
/**
@ -41,6 +42,8 @@ export function RoomShareButtons({ joinCode, shareUrl }: RoomShareButtonsProps)
}
copiedLabel="Link Copied!"
/>
<QRCodeButton url={shareUrl} />
</>
)
}