206 lines
6.1 KiB
TypeScript
206 lines
6.1 KiB
TypeScript
|
|
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
|