Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
parkpow
GitHub Repository: parkpow/deep-license-plate-recognition
Path: blob/master/gate-controller/components/ConfiguredRelayCard.tsx
1072 views
1
"use client";
2
3
import { invoke } from "@tauri-apps/api/core";
4
import { Button } from "@/components/ui/button";
5
import { Card, CardContent, CardHeader } from "@/components/ui/card";
6
import {
7
AlertDialog,
8
AlertDialogAction,
9
AlertDialogCancel,
10
AlertDialogContent,
11
AlertDialogDescription,
12
AlertDialogFooter,
13
AlertDialogHeader,
14
AlertDialogTitle,
15
AlertDialogTrigger,
16
} from "@/components/ui/alert-dialog";
17
import { useState } from "react";
18
import { toast } from "sonner";
19
import {
20
HardDrive,
21
Usb,
22
Trash2,
23
ClipboardCopy,
24
ChevronDown,
25
ChevronUp,
26
} from "lucide-react";
27
import { ApiInstructionsModal } from "@/components/ApiInstructionsModal";
28
29
interface ConfiguredRelay {
30
id: string;
31
type: "ch340" | "hw348" | "cp210x";
32
channels?: number;
33
}
34
35
interface ConfiguredRelayCardProps {
36
relay: ConfiguredRelay;
37
onRelayRemoved: () => void;
38
}
39
40
export function ConfiguredRelayCard({
41
relay,
42
onRelayRemoved,
43
}: ConfiguredRelayCardProps) {
44
const [showApiInstructions, setShowApiInstructions] = useState(false);
45
const [isExpanded, setIsExpanded] = useState(false);
46
47
const handleAction = async (
48
action: "on" | "off",
49
channel: number | null = null,
50
) => {
51
try {
52
await invoke("trigger_relay_action", {
53
payload: { id: relay.id, action, channel },
54
});
55
toast.success(
56
`Action '${action}' sent to ${relay.id}${
57
channel ? ` on channel ${channel}` : ""
58
}`,
59
);
60
} catch (error) {
61
toast.error(`Failed to trigger ${relay.id}`, {
62
description: String(error),
63
});
64
}
65
};
66
67
const handleRemove = async () => {
68
try {
69
await invoke("remove_relay", { id: relay.id });
70
toast.success("Relay Removed", {
71
description: `${relay.id} has been removed from configuration.`,
72
});
73
onRelayRemoved();
74
} catch (error) {
75
toast.error("Failed to remove relay", { description: String(error) });
76
}
77
};
78
79
const isSerial = relay.type === "ch340" || relay.type === "cp210x";
80
81
return (
82
<Card className="hover:shadow-md transition-shadow">
83
<CardHeader className="p-3">
84
<div className="flex items-center justify-between">
85
<div className="flex items-center gap-2 flex-1 min-w-0">
86
<div className="flex-shrink-0">
87
{isSerial ? (
88
<div className="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center">
89
<HardDrive className="w-4 h-4 text-primary" />
90
</div>
91
) : (
92
<div className="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center">
93
<Usb className="w-4 h-4 text-primary" />
94
</div>
95
)}
96
</div>
97
<div className="flex-1 min-w-0">
98
<h3 className="text-sm font-semibold truncate">{relay.id}</h3>
99
<p className="text-xs text-muted-foreground">
100
{relay.type === "ch340" && `CH340`}
101
{relay.type === "hw348" && `HW-348`}
102
{relay.type === "cp210x" && `CP210x`}
103
{relay.channels && ` • ${relay.channels}ch`}
104
</p>
105
</div>
106
</div>
107
<div className="flex items-center gap-1 flex-shrink-0">
108
{relay.channels && relay.channels > 0 && (
109
<Button
110
variant="ghost"
111
size="sm"
112
className="h-8 w-8 p-0"
113
onClick={() => setIsExpanded(!isExpanded)}
114
>
115
{isExpanded ? (
116
<ChevronUp className="w-4 h-4" />
117
) : (
118
<ChevronDown className="w-4 h-4" />
119
)}
120
</Button>
121
)}
122
<Button
123
variant="ghost"
124
size="sm"
125
className="h-8 w-8 p-0"
126
onClick={() => setShowApiInstructions(true)}
127
>
128
<ClipboardCopy className="w-4 h-4" />
129
</Button>
130
<AlertDialog>
131
<AlertDialogTrigger asChild>
132
<Button
133
variant="ghost"
134
size="sm"
135
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
136
>
137
<Trash2 className="w-4 h-4" />
138
</Button>
139
</AlertDialogTrigger>
140
<AlertDialogContent>
141
<AlertDialogHeader>
142
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
143
<AlertDialogDescription>
144
This will permanently remove the relay{" "}
145
<strong>{relay.id}</strong> from your configuration.
146
</AlertDialogDescription>
147
</AlertDialogHeader>
148
<AlertDialogFooter>
149
<AlertDialogCancel>Cancel</AlertDialogCancel>
150
<AlertDialogAction onClick={handleRemove}>
151
Continue
152
</AlertDialogAction>
153
</AlertDialogFooter>
154
</AlertDialogContent>
155
</AlertDialog>
156
</div>
157
</div>
158
</CardHeader>
159
160
{isExpanded &&
161
relay.channels &&
162
relay.channels > 0 && (
163
<CardContent className="p-3 pt-0">
164
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
165
{[...Array(relay.channels)].map((_, i) => {
166
const channel = i + 1;
167
return (
168
<div key={channel} className="flex flex-col gap-1">
169
<span className="text-xs font-mono text-center text-muted-foreground">
170
Ch {channel}
171
</span>
172
<Button
173
size="sm"
174
className="bg-green-600 hover:bg-green-700 text-xs h-7 px-2"
175
onClick={() => handleAction("on", channel)}
176
>
177
ON
178
</Button>
179
<Button
180
size="sm"
181
className="bg-red-600 hover:bg-red-700 text-xs h-7 px-2"
182
onClick={() => handleAction("off", channel)}
183
>
184
OFF
185
</Button>
186
</div>
187
);
188
})}
189
</div>
190
</CardContent>
191
)}
192
193
<ApiInstructionsModal
194
relay={relay}
195
isOpen={showApiInstructions}
196
onClose={() => setShowApiInstructions(false)}
197
/>
198
</Card>
199
);
200
}
201
202