|
@@ -1,7 +1,9 @@
|
|
|
|
|
+import { useState } from 'react';
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
import { Plug, Power, PowerOff, Loader2, Wifi, WifiOff, Zap } from 'lucide-react';
|
|
import { Plug, Power, PowerOff, Loader2, Wifi, WifiOff, Zap } from 'lucide-react';
|
|
|
import { api } from '../api/client';
|
|
import { api } from '../api/client';
|
|
|
import type { SmartPlug } from '../api/client';
|
|
import type { SmartPlug } from '../api/client';
|
|
|
|
|
+import { ConfirmModal } from './ConfirmModal';
|
|
|
|
|
|
|
|
interface SwitchbarPopoverProps {
|
|
interface SwitchbarPopoverProps {
|
|
|
onClose: () => void;
|
|
onClose: () => void;
|
|
@@ -9,6 +11,7 @@ interface SwitchbarPopoverProps {
|
|
|
|
|
|
|
|
function SwitchItem({ plug }: { plug: SmartPlug }) {
|
|
function SwitchItem({ plug }: { plug: SmartPlug }) {
|
|
|
const queryClient = useQueryClient();
|
|
const queryClient = useQueryClient();
|
|
|
|
|
+ const [confirmAction, setConfirmAction] = useState<'on' | 'off' | null>(null);
|
|
|
|
|
|
|
|
// Fetch current status
|
|
// Fetch current status
|
|
|
const { data: status, isLoading: statusLoading } = useQuery({
|
|
const { data: status, isLoading: statusLoading } = useQuery({
|
|
@@ -29,66 +32,86 @@ function SwitchItem({ plug }: { plug: SmartPlug }) {
|
|
|
const isReachable = status?.reachable ?? false;
|
|
const isReachable = status?.reachable ?? false;
|
|
|
const isPending = controlMutation.isPending;
|
|
const isPending = controlMutation.isPending;
|
|
|
|
|
|
|
|
|
|
+ const handleConfirm = () => {
|
|
|
|
|
+ if (confirmAction) {
|
|
|
|
|
+ controlMutation.mutate(confirmAction);
|
|
|
|
|
+ setConfirmAction(null);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
- <div className="flex items-center justify-between py-2 px-3 hover:bg-bambu-dark-tertiary rounded-lg transition-colors">
|
|
|
|
|
- <div className="flex items-center gap-2">
|
|
|
|
|
- <div className={`p-1.5 rounded ${isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20'}`}>
|
|
|
|
|
- <Plug className={`w-4 h-4 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <p className="text-sm text-white font-medium">{plug.name}</p>
|
|
|
|
|
- <div className="flex items-center gap-1 text-xs">
|
|
|
|
|
- {statusLoading ? (
|
|
|
|
|
- <Loader2 className="w-3 h-3 text-bambu-gray animate-spin" />
|
|
|
|
|
- ) : isReachable ? (
|
|
|
|
|
- <>
|
|
|
|
|
- <Wifi className="w-3 h-3 text-bambu-green" />
|
|
|
|
|
- <span className={isOn ? 'text-bambu-green' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
|
|
|
|
|
- {status?.energy?.power !== null && status?.energy?.power !== undefined && (
|
|
|
|
|
- <>
|
|
|
|
|
- <span className="text-bambu-gray mx-1">|</span>
|
|
|
|
|
- <Zap className="w-3 h-3 text-yellow-400" />
|
|
|
|
|
- <span className="text-yellow-400">{Math.round(status.energy.power)}W</span>
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
- </>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <>
|
|
|
|
|
- <WifiOff className="w-3 h-3 text-red-400" />
|
|
|
|
|
- <span className="text-red-400">Offline</span>
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div className="flex items-center justify-between py-2 px-3 hover:bg-bambu-dark-tertiary rounded-lg transition-colors">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <div className={`p-1.5 rounded ${isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20'}`}>
|
|
|
|
|
+ <Plug className={`w-4 h-4 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-sm text-white font-medium">{plug.name}</p>
|
|
|
|
|
+ <div className="flex items-center gap-1 text-xs">
|
|
|
|
|
+ {statusLoading ? (
|
|
|
|
|
+ <Loader2 className="w-3 h-3 text-bambu-gray animate-spin" />
|
|
|
|
|
+ ) : isReachable ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Wifi className="w-3 h-3 text-bambu-green" />
|
|
|
|
|
+ <span className={isOn ? 'text-bambu-green' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
|
|
|
|
|
+ {status?.energy?.power !== null && status?.energy?.power !== undefined && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <span className="text-bambu-gray mx-1">|</span>
|
|
|
|
|
+ <Zap className="w-3 h-3 text-yellow-400" />
|
|
|
|
|
+ <span className="text-yellow-400">{Math.round(status.energy.power)}W</span>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <WifiOff className="w-3 h-3 text-red-400" />
|
|
|
|
|
+ <span className="text-red-400">Offline</span>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- <div className="flex gap-1">
|
|
|
|
|
- <button
|
|
|
|
|
- onClick={() => controlMutation.mutate('on')}
|
|
|
|
|
- disabled={!isReachable || isPending}
|
|
|
|
|
- className={`p-1.5 rounded transition-colors ${
|
|
|
|
|
- isOn
|
|
|
|
|
- ? 'bg-bambu-green text-white'
|
|
|
|
|
- : 'bg-bambu-dark text-bambu-gray hover:text-white'
|
|
|
|
|
- } disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
|
|
|
- title="Turn On"
|
|
|
|
|
- >
|
|
|
|
|
- {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
|
|
|
|
|
- </button>
|
|
|
|
|
- <button
|
|
|
|
|
- onClick={() => controlMutation.mutate('off')}
|
|
|
|
|
- disabled={!isReachable || isPending}
|
|
|
|
|
- className={`p-1.5 rounded transition-colors ${
|
|
|
|
|
- !isOn && isReachable
|
|
|
|
|
- ? 'bg-bambu-dark-tertiary text-white'
|
|
|
|
|
- : 'bg-bambu-dark text-bambu-gray hover:text-white'
|
|
|
|
|
- } disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
|
|
|
- title="Turn Off"
|
|
|
|
|
- >
|
|
|
|
|
- {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
|
|
|
|
|
- </button>
|
|
|
|
|
|
|
+ <div className="flex gap-1">
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setConfirmAction('on')}
|
|
|
|
|
+ disabled={!isReachable || isPending}
|
|
|
|
|
+ className={`p-1.5 rounded transition-colors ${
|
|
|
|
|
+ isOn
|
|
|
|
|
+ ? 'bg-bambu-green text-white'
|
|
|
|
|
+ : 'bg-bambu-dark text-bambu-gray hover:text-white'
|
|
|
|
|
+ } disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
|
|
|
+ title="Turn On"
|
|
|
|
|
+ >
|
|
|
|
|
+ {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setConfirmAction('off')}
|
|
|
|
|
+ disabled={!isReachable || isPending}
|
|
|
|
|
+ className={`p-1.5 rounded transition-colors ${
|
|
|
|
|
+ !isOn && isReachable
|
|
|
|
|
+ ? 'bg-bambu-dark-tertiary text-white'
|
|
|
|
|
+ : 'bg-bambu-dark text-bambu-gray hover:text-white'
|
|
|
|
|
+ } disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
|
|
|
+ title="Turn Off"
|
|
|
|
|
+ >
|
|
|
|
|
+ {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ {confirmAction && (
|
|
|
|
|
+ <ConfirmModal
|
|
|
|
|
+ title={`Turn ${confirmAction === 'on' ? 'On' : 'Off'} Smart Plug`}
|
|
|
|
|
+ message={`Are you sure you want to turn ${confirmAction === 'on' ? 'on' : 'off'} "${plug.name}"?`}
|
|
|
|
|
+ confirmText={confirmAction === 'on' ? 'Turn On' : 'Turn Off'}
|
|
|
|
|
+ variant={confirmAction === 'off' ? 'warning' : 'default'}
|
|
|
|
|
+ onConfirm={handleConfirm}
|
|
|
|
|
+ onCancel={() => setConfirmAction(null)}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|