gsproremote/frontend/src/components/QuickActions.tsx

206 lines
6.1 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react'
import { actionsAPI } from '../api/client'
import toast from 'react-hot-toast'
import { useStore } from '../stores/appStore'
interface QuickActionsProps {
mobile?: boolean
}
const QuickActions: React.FC<QuickActionsProps> = ({ mobile = false }) => {
const [isOpen, setIsOpen] = useState(false)
const { settings } = useStore()
const actions = [
{
id: 'scorecard',
label: 'Scorecard',
icon: '📊',
key: 't',
description: 'Toggle scorecard',
},
{
id: 'rangefinder',
label: 'Range Finder',
icon: '🎯',
key: 'r',
description: 'Launch range finder',
},
{
id: 'heatmap',
label: 'Heat Map',
icon: '🗺️',
key: 'y',
description: 'Toggle heat map',
},
{
id: 'flyover',
label: 'Flyover',
icon: '🚁',
key: 'o',
description: 'Hole preview',
},
{
id: 'pin',
label: 'Pin Indicator',
icon: '🚩',
key: 'p',
description: 'Show/hide pin',
},
{
id: 'freelook',
label: 'Free Look',
icon: '👀',
key: 'f5',
description: 'Unlock camera',
},
{
id: 'aimpoint',
label: 'Aim Point',
icon: '🎪',
key: 'f3',
description: 'Toggle aim point',
},
{
id: 'tracerclear',
label: 'Clear Tracer',
icon: '💨',
key: 'f1',
description: 'Clear ball tracer',
},
]
const handleAction = async (action: typeof actions[0]) => {
try {
await actionsAPI.sendKey(action.key)
toast.success(action.description)
setIsOpen(false)
// Haptic feedback
if (settings.hapticFeedback && 'vibrate' in navigator) {
navigator.vibrate(10)
}
} catch (error) {
toast.error(`Failed to execute ${action.label}`)
}
}
const handleToggle = () => {
setIsOpen(!isOpen)
// Haptic feedback
if (settings.hapticFeedback && 'vibrate' in navigator) {
navigator.vibrate(10)
}
}
if (mobile) {
// Mobile version - bottom sheet style
return (
<>
{/* Floating Action Button */}
<button
onClick={handleToggle}
className={`fixed bottom-20 right-4 z-40 w-14 h-14 bg-primary-600 hover:bg-primary-700 text-white rounded-full shadow-lg transition-all transform ${
isOpen ? 'rotate-45 scale-110' : ''
}`}
aria-label="Quick Actions"
>
<svg className="w-8 h-8 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</button>
{/* Bottom Sheet */}
<div
className={`fixed inset-x-0 bottom-0 z-30 bg-gray-800 rounded-t-2xl shadow-2xl transition-transform duration-300 ${
isOpen ? 'translate-y-0' : 'translate-y-full'
}`}
>
<div className="p-4">
<div className="w-12 h-1 bg-gray-600 rounded-full mx-auto mb-4"></div>
<h3 className="text-lg font-semibold text-white mb-3">Quick Actions</h3>
<div className="grid grid-cols-3 gap-3">
{actions.map((action) => (
<button
key={action.id}
onClick={() => handleAction(action)}
className="bg-gray-700 hover:bg-gray-600 p-3 rounded-lg text-center transition-colors"
>
<div className="text-2xl mb-1">{action.icon}</div>
<div className="text-xs text-gray-300">{action.label}</div>
</button>
))}
</div>
</div>
</div>
{/* Backdrop */}
{isOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-20"
onClick={() => setIsOpen(false)}
/>
)}
</>
)
}
// Desktop version - floating menu
return (
<div className="fixed bottom-8 right-8 z-40">
{/* Action Menu */}
<div
className={`absolute bottom-16 right-0 bg-gray-800 rounded-lg shadow-2xl overflow-hidden transition-all duration-300 ${
isOpen
? 'opacity-100 scale-100 translate-y-0'
: 'opacity-0 scale-95 translate-y-4 pointer-events-none'
}`}
>
<div className="p-2 max-h-96 overflow-y-auto">
<div className="space-y-1">
{actions.map((action) => (
<button
key={action.id}
onClick={() => handleAction(action)}
className="w-full flex items-center space-x-3 px-3 py-2 hover:bg-gray-700 rounded-lg transition-colors group"
>
<span className="text-xl">{action.icon}</span>
<div className="text-left flex-1">
<div className="text-sm font-medium text-white group-hover:text-primary-400 transition-colors">
{action.label}
</div>
<div className="text-xs text-gray-400">{action.description}</div>
</div>
<kbd className="hidden sm:inline-block px-2 py-1 text-xs bg-gray-900 rounded">
{action.key.toUpperCase()}
</kbd>
</button>
))}
</div>
</div>
</div>
{/* Floating Action Button */}
<button
onClick={handleToggle}
className={`relative w-14 h-14 bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 text-white rounded-full shadow-lg transition-all transform hover:scale-110 ${
isOpen ? 'rotate-45' : ''
}`}
aria-label="Quick Actions"
>
<svg className="w-8 h-8 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{/* Pulse animation when closed */}
{!isOpen && (
<span className="absolute inset-0 rounded-full bg-primary-600 animate-ping opacity-30"></span>
)}
</button>
</div>
)
}
export default QuickActions