feat: Implement target categorization and filtering in the Profile Editor UI.

This commit is contained in:
Ben Allfree
2025-11-22 17:03:30 -08:00
parent 9e30289fbf
commit 9943a32bf0
+109 -22
View File
@@ -4,6 +4,8 @@ import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { api } from "../../convex/_generated/api";
import { TARGETS } from "../constants/targets";
import { VERSIONS } from "../constants/versions";
interface ProfileEditorProps {
initialData?: any;
@@ -27,11 +29,41 @@ export default function ProfileEditor({
MESHTASTIC_EXCLUDE_MQTT: false,
MESHTASTIC_EXCLUDE_AUDIO: false,
},
version: VERSIONS[0],
},
});
const targets = watch("targets");
// Group targets by category
const groupedTargets = React.useMemo(() => {
return Object.entries(TARGETS).reduce(
(acc, [id, meta]) => {
const category = meta.category || "Other";
if (!acc[category]) acc[category] = [];
acc[category].push({ id, ...meta });
return acc;
},
{} as Record<string, ((typeof TARGETS)[string] & { id: string })[]>,
);
}, []);
const categories = React.useMemo(
() => Object.keys(groupedTargets).sort(),
[groupedTargets],
);
const [activeCategory, setActiveCategory] = React.useState<string>(
categories[0] || "Heltec",
);
// Update active category when categories load if not set
React.useEffect(() => {
if (!activeCategory && categories.length > 0) {
setActiveCategory(categories[0]);
}
}, [categories, activeCategory]);
const toggleTarget = (target: string) => {
const current = targets || [];
if (current.includes(target)) {
@@ -51,6 +83,7 @@ export default function ProfileEditor({
name: data.name,
targets: data.targets,
config: data.config,
version: data.version,
});
} else {
await createProfile(data);
@@ -63,33 +96,87 @@ export default function ProfileEditor({
onSubmit={handleSubmit(onSubmit)}
className="space-y-6 bg-slate-900 p-6 rounded-lg border border-slate-800"
>
<div>
<label className="block text-sm font-medium mb-2">Profile Name</label>
<Input
{...register("name")}
className="bg-slate-950 border-slate-800"
placeholder="e.g. Solar Repeater"
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium mb-2">Profile Name</label>
<Input
{...register("name")}
className="bg-slate-950 border-slate-800"
placeholder="e.g. Solar Repeater"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Firmware Version
</label>
<select
{...register("version")}
className="w-full h-10 px-3 rounded-md border border-slate-800 bg-slate-950 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-950"
>
{VERSIONS.map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2">Targets</label>
<div className="flex gap-4 flex-wrap">
{["tbeam", "rak4631", "heltec_v3", "pico"].map((t) => (
<div key={t} className="flex items-center space-x-2">
<Checkbox
id={t}
checked={targets?.includes(t)}
onCheckedChange={() => toggleTarget(t)}
/>
<label
htmlFor={t}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t}
</label>
{/* ... existing target UI */}
<div className="space-y-4">
{/* Category Pills */}
<div className="flex flex-wrap gap-2">
{categories.map((category) => {
const count = groupedTargets[category].filter((t) =>
targets?.includes(t.id),
).length;
const isActive = activeCategory === category;
return (
<button
key={category}
type="button"
onClick={() => setActiveCategory(category)}
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${
isActive
? "bg-blue-600 text-white"
: "bg-slate-800 text-slate-300 hover:bg-slate-700"
}`}
>
{category}
{count > 0 && (
<span className="ml-2 bg-white/20 px-1.5 py-0.5 rounded-full text-xs">
{count}
</span>
)}
</button>
);
})}
</div>
{/* Active Category Targets */}
<div className="bg-slate-950/50 p-4 rounded-lg border border-slate-800/50">
<div className="flex gap-4 flex-wrap">
{groupedTargets[activeCategory]?.map((item) => (
<div key={item.id} className="flex items-center space-x-2">
<Checkbox
id={item.id}
checked={targets?.includes(item.id)}
onCheckedChange={() => toggleTarget(item.id)}
/>
<label
htmlFor={item.id}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{item.name}
</label>
</div>
))}
</div>
))}
</div>
</div>
</div>