SwitchbarPopover.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { useState } from 'react';
  2. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  3. import { Plug, Power, PowerOff, Loader2, Wifi, WifiOff, Zap } from 'lucide-react';
  4. import { api } from '../api/client';
  5. import type { SmartPlug } from '../api/client';
  6. import { ConfirmModal } from './ConfirmModal';
  7. interface SwitchbarPopoverProps {
  8. onClose: () => void;
  9. }
  10. function SwitchItem({ plug }: { plug: SmartPlug }) {
  11. const queryClient = useQueryClient();
  12. const [confirmAction, setConfirmAction] = useState<'on' | 'off' | null>(null);
  13. // Fetch current status
  14. const { data: status, isLoading: statusLoading } = useQuery({
  15. queryKey: ['smart-plug-status', plug.id],
  16. queryFn: () => api.getSmartPlugStatus(plug.id),
  17. refetchInterval: 10000, // Refresh every 10 seconds when popover is open
  18. });
  19. // Control mutation
  20. const controlMutation = useMutation({
  21. mutationFn: (action: 'on' | 'off') => api.controlSmartPlug(plug.id, action),
  22. onSuccess: () => {
  23. queryClient.invalidateQueries({ queryKey: ['smart-plug-status', plug.id] });
  24. },
  25. });
  26. const isOn = status?.state === 'ON';
  27. const isReachable = status?.reachable ?? false;
  28. const isPending = controlMutation.isPending;
  29. const handleConfirm = () => {
  30. if (confirmAction) {
  31. controlMutation.mutate(confirmAction);
  32. setConfirmAction(null);
  33. }
  34. };
  35. return (
  36. <>
  37. <div className="flex items-center justify-between py-2 px-3 hover:bg-bambu-dark-tertiary rounded-lg transition-colors">
  38. <div className="flex items-center gap-2">
  39. <div className={`p-1.5 rounded ${isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20'}`}>
  40. <Plug className={`w-4 h-4 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
  41. </div>
  42. <div>
  43. <p className="text-sm text-white font-medium">{plug.name}</p>
  44. <div className="flex items-center gap-1 text-xs">
  45. {statusLoading ? (
  46. <Loader2 className="w-3 h-3 text-bambu-gray animate-spin" />
  47. ) : isReachable ? (
  48. <>
  49. <Wifi className="w-3 h-3 text-bambu-green" />
  50. <span className={isOn ? 'text-bambu-green' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
  51. {status?.energy?.power !== null && status?.energy?.power !== undefined && (
  52. <>
  53. <span className="text-bambu-gray mx-1">|</span>
  54. <Zap className="w-3 h-3 text-yellow-400" />
  55. <span className="text-yellow-400">{Math.round(status.energy.power)}W</span>
  56. </>
  57. )}
  58. </>
  59. ) : (
  60. <>
  61. <WifiOff className="w-3 h-3 text-red-400" />
  62. <span className="text-red-400">Offline</span>
  63. </>
  64. )}
  65. </div>
  66. </div>
  67. </div>
  68. <div className="flex gap-1">
  69. <button
  70. onClick={() => setConfirmAction('on')}
  71. disabled={!isReachable || isPending}
  72. className={`p-1.5 rounded transition-colors ${
  73. isOn
  74. ? 'bg-bambu-green text-white'
  75. : 'bg-bambu-dark text-bambu-gray hover:text-white'
  76. } disabled:opacity-50 disabled:cursor-not-allowed`}
  77. title="Turn On"
  78. >
  79. {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
  80. </button>
  81. <button
  82. onClick={() => setConfirmAction('off')}
  83. disabled={!isReachable || isPending}
  84. className={`p-1.5 rounded transition-colors ${
  85. !isOn && isReachable
  86. ? 'bg-bambu-dark-tertiary text-white'
  87. : 'bg-bambu-dark text-bambu-gray hover:text-white'
  88. } disabled:opacity-50 disabled:cursor-not-allowed`}
  89. title="Turn Off"
  90. >
  91. {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
  92. </button>
  93. </div>
  94. </div>
  95. {confirmAction && (
  96. <ConfirmModal
  97. title={`Turn ${confirmAction === 'on' ? 'On' : 'Off'} Smart Plug`}
  98. message={`Are you sure you want to turn ${confirmAction === 'on' ? 'on' : 'off'} "${plug.name}"?`}
  99. confirmText={confirmAction === 'on' ? 'Turn On' : 'Turn Off'}
  100. variant={confirmAction === 'off' ? 'warning' : 'default'}
  101. onConfirm={handleConfirm}
  102. onCancel={() => setConfirmAction(null)}
  103. />
  104. )}
  105. </>
  106. );
  107. }
  108. export function SwitchbarPopover({ onClose }: SwitchbarPopoverProps) {
  109. // Fetch all smart plugs
  110. const { data: plugs, isLoading } = useQuery({
  111. queryKey: ['smart-plugs'],
  112. queryFn: api.getSmartPlugs,
  113. });
  114. // Filter to only show plugs with show_in_switchbar enabled
  115. const switchbarPlugs = plugs?.filter(p => p.show_in_switchbar) || [];
  116. return (
  117. <div
  118. className="absolute bottom-full left-0 mb-2 w-72 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl shadow-xl z-50"
  119. onMouseLeave={onClose}
  120. >
  121. {/* Header */}
  122. <div className="px-4 py-3 border-b border-bambu-dark-tertiary">
  123. <h3 className="text-sm font-semibold text-white flex items-center gap-2">
  124. <Plug className="w-4 h-4 text-bambu-green" />
  125. Smart Switches
  126. </h3>
  127. </div>
  128. {/* Content */}
  129. <div className="p-2 max-h-80 overflow-y-auto">
  130. {isLoading ? (
  131. <div className="flex items-center justify-center py-8">
  132. <Loader2 className="w-6 h-6 text-bambu-gray animate-spin" />
  133. </div>
  134. ) : switchbarPlugs.length === 0 ? (
  135. <div className="text-center py-6 px-4">
  136. <Plug className="w-8 h-8 text-bambu-gray mx-auto mb-2" />
  137. <p className="text-sm text-bambu-gray">No switches in switchbar</p>
  138. <p className="text-xs text-bambu-gray mt-1">
  139. Enable "Show in Switchbar" in Settings &gt; Smart Plugs
  140. </p>
  141. </div>
  142. ) : (
  143. <div className="space-y-1">
  144. {switchbarPlugs.map(plug => (
  145. <SwitchItem key={plug.id} plug={plug} />
  146. ))}
  147. </div>
  148. )}
  149. </div>
  150. </div>
  151. );
  152. }