Initial commit: GSPro Remote MVP - Phase 1 complete
This commit is contained in:
commit
74ca4b38eb
50 changed files with 12818 additions and 0 deletions
205
frontend/src/components/QuickActions.tsx
Normal file
205
frontend/src/components/QuickActions.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue