Initial implementation of Bluetooth toy control app
CI / Static checks (push) Successful in 1m27s
CI / Build and push image (push) Skipped

Next.js app with a browser-side buttplug/buttplug-wasm control layer
(server never touches real-time device commands), SQLite storage via
Drizzle, single-secret auth, recordings/replay with device remapping,
a usage stats dashboard, Docker deployment, and Gitea CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8WkFF5ppURBAB918593Eb
This commit is contained in:
2026-08-25 07:51:38 +02:00
co-authored by Claude Sonnet 5
commit 1119c8eea0
112 changed files with 15522 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useTheme } from "next-themes";
import { Menu, Moon, Sun, LogOut } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import { ConnectionStatus } from "./ConnectionStatus";
import { cn } from "@/lib/utils";
const LINKS = [
{ href: "/", label: "Dashboard" },
{ href: "/control", label: "Control" },
{ href: "/recordings", label: "Recordings" },
{ href: "/sessions", label: "Sessions" },
{ href: "/stats", label: "Stats" },
{ href: "/devices", label: "Devices" },
];
function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
const pathname = usePathname();
return (
<>
{LINKS.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={onNavigate}
className={cn(
"rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
pathname === link.href
? "bg-primary/15 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
{link.label}
</Link>
))}
</>
);
}
export function NavBar() {
const { theme, setTheme } = useTheme();
const router = useRouter();
async function handleLogout() {
await fetch("/api/auth/logout", { method: "POST" });
router.push("/login");
router.refresh();
}
return (
<header className="sticky top-0 z-40 border-b border-border bg-background/80 backdrop-blur-lg">
<div className="mx-auto flex h-14 max-w-6xl items-center gap-3 px-4">
<Link href="/" className="font-display bp-gradient-text mr-2 text-lg font-semibold">
sexy
</Link>
<nav className="hidden items-center gap-1 md:flex">
<NavLinks />
</nav>
<div className="ml-auto flex items-center gap-2">
<ConnectionStatus />
<Button
variant="ghost"
size="icon"
aria-label="Toggle theme"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<Sun className="hidden size-4 dark:block" />
<Moon className="block size-4 dark:hidden" />
</Button>
<Button variant="ghost" size="icon" aria-label="Log out" onClick={handleLogout}>
<LogOut className="size-4" />
</Button>
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden" aria-label="Open menu">
<Menu className="size-4" />
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-64">
<SheetTitle className="px-4 pt-4">Menu</SheetTitle>
<nav className="flex flex-col gap-1 p-4">
<NavLinks />
</nav>
</SheetContent>
</Sheet>
</div>
</div>
</header>
);
}