initial commit

This commit is contained in:
Ben Allfree
2025-11-22 05:25:45 -08:00
commit 62c1cd8d01
38 changed files with 2214 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
import { useMutation } from "convex/react";
import { useForm } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { api } from "../../convex/_generated/api";
interface ProfileEditorProps {
initialData?: any;
onSave: () => void;
onCancel: () => void;
}
export default function ProfileEditor({
initialData,
onSave,
onCancel,
}: ProfileEditorProps) {
const createProfile = useMutation(api.profiles.create);
const updateProfile = useMutation(api.profiles.update);
const { register, handleSubmit, setValue, watch } = useForm({
defaultValues: initialData || {
name: "",
targets: [],
config: {
MESHTASTIC_EXCLUDE_MQTT: false,
MESHTASTIC_EXCLUDE_AUDIO: false,
},
},
});
const targets = watch("targets");
const toggleTarget = (target: string) => {
const current = targets || [];
if (current.includes(target)) {
setValue(
"targets",
current.filter((t: string) => t !== target),
);
} else {
setValue("targets", [...current, target]);
}
};
const onSubmit = async (data: any) => {
if (initialData?._id) {
await updateProfile({ id: initialData._id, ...data });
} else {
await createProfile(data);
}
onSave();
};
return (
<form
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>
<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>
</div>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Configuration Flags
</label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="no_mqtt"
checked={watch("config.MESHTASTIC_EXCLUDE_MQTT")}
onCheckedChange={(checked) =>
setValue("config.MESHTASTIC_EXCLUDE_MQTT", checked)
}
/>
<label htmlFor="no_mqtt">Exclude MQTT</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="no_audio"
checked={watch("config.MESHTASTIC_EXCLUDE_AUDIO")}
onCheckedChange={(checked) =>
setValue("config.MESHTASTIC_EXCLUDE_AUDIO", checked)
}
/>
<label htmlFor="no_audio">Exclude Audio</label>
</div>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button type="button" variant="ghost" onClick={onCancel}>
Cancel
</Button>
<Button type="submit">Save Profile</Button>
</div>
</form>
);
}
+57
View File
@@ -0,0 +1,57 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
+28
View File
@@ -0,0 +1,28 @@
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("grid place-content-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };