Compare commits
6
Commits
afa6e8d473
...
v0.8.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27d0db0f72 | ||
|
|
e436ced95b | ||
|
|
fe017497a2 | ||
|
|
fc7fd74d17 | ||
|
|
8c3d4e4e33 | ||
|
|
581c3c0e6c |
@@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { SessionTimelineChart } from "@/components/sessions/SessionTimelineChart";
|
||||
import { SessionTitleEditor } from "@/components/sessions/SessionTitleEditor";
|
||||
import { SessionDescriptionEditor } from "@/components/sessions/SessionDescriptionEditor";
|
||||
import { SessionDeleteButton } from "@/components/sessions/SessionDeleteButton";
|
||||
import { STATUS_VARIANT } from "@/components/sessions/session-status";
|
||||
import { getSessionDetail, getSessionName } from "@/lib/db/queries/sessions";
|
||||
import { getSessionTimeline } from "@/lib/db/queries/stats";
|
||||
@@ -65,6 +66,7 @@ export default async function SessionDetailPage({ params }: { params: Promise<{
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<SessionDeleteButton sessionId={detail.session.id} sessionName={detail.session.name} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ export function ButtplugConsole() {
|
||||
|
||||
{Object.keys(devices).length === 0 ? (
|
||||
<Card className="bp-glass">
|
||||
<CardContent className="py-6 text-sm text-muted-foreground">
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
No devices connected yet. Scan to discover nearby toys, then select one from your browser's
|
||||
pairing prompt.
|
||||
</CardContent>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Check, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -55,23 +54,23 @@ function DeviceNameCell({ device }: { device: DeviceRow }) {
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<div className="relative w-48">
|
||||
<Input
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
className="h-8 pr-8"
|
||||
aria-label="Device display name"
|
||||
className="max-w-48 min-w-0 border-b-2 border-transparent bg-transparent text-sm font-medium text-foreground caret-primary outline-none transition-colors placeholder:text-muted-foreground/70 hover:border-b-primary focus:border-b-primary [field-sizing:content]"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-2 flex w-3.5 items-center justify-center">
|
||||
<div className="relative flex size-4 shrink-0 items-center justify-center">
|
||||
<Loader2
|
||||
className={cn(
|
||||
"absolute size-3.5 animate-spin text-muted-foreground transition-opacity",
|
||||
"absolute size-4 animate-spin text-muted-foreground transition-opacity",
|
||||
status === "saving" ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<Check
|
||||
className={cn(
|
||||
"absolute size-3.5 text-primary transition-opacity",
|
||||
"absolute size-4 text-primary transition-opacity",
|
||||
status === "saved" ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -15,8 +15,8 @@ const LINKS = [
|
||||
{ href: "/", label: "Dashboard" },
|
||||
{ href: "/control", label: "Control" },
|
||||
{ href: "/sessions", label: "Sessions" },
|
||||
{ href: "/stats", label: "Stats" },
|
||||
{ href: "/devices", label: "Devices" },
|
||||
{ href: "/stats", label: "Stats" },
|
||||
];
|
||||
|
||||
function isActive(pathname: string, href: string): boolean {
|
||||
|
||||
@@ -164,7 +164,7 @@ export function ReplayPlayer({ sessionId }: { sessionId: number }) {
|
||||
|
||||
{connectedDevices.length === 0 && (
|
||||
<Card className="bp-glass">
|
||||
<CardContent className="py-6 text-sm text-muted-foreground">
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
No devices connected yet. Scan to discover nearby toys, then match them to this session's
|
||||
original device slots.
|
||||
</CardContent>
|
||||
@@ -173,7 +173,7 @@ export function ReplayPlayer({ sessionId }: { sessionId: number }) {
|
||||
</>
|
||||
) : (
|
||||
<Card className="bp-glass">
|
||||
<CardContent className="flex flex-col gap-3 py-6">
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
{replayTargets.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{replayTargets.map((t) => (
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function SessionDeleteButton({ sessionId, sessionName }: { sessionId: number; sessionName: string | null }) {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
async function handleDelete() {
|
||||
setDeleting(true);
|
||||
const res = await fetch(`/api/sessions/${sessionId}`, { method: "DELETE" });
|
||||
setDeleting(false);
|
||||
if (res.ok) {
|
||||
toast.success("Session deleted");
|
||||
router.push("/sessions");
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error("Could not delete session");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="ghost" size="icon-sm" onClick={() => setOpen(true)} aria-label="Delete session">
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete session?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This permanently deletes{" "}
|
||||
<span className="font-medium text-foreground">{sessionName ?? `Session #${sessionId}`}</span> and all
|
||||
of its recorded events. This can't be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={() => void handleDelete()} disabled={deleting}>
|
||||
{deleting ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export function SessionDescriptionEditor({
|
||||
placeholder="Add a description..."
|
||||
aria-label="Session description"
|
||||
rows={1}
|
||||
className="max-w-full min-w-0 resize-none border-b-2 border-transparent bg-transparent text-sm text-muted-foreground outline-none transition-colors placeholder:text-muted-foreground/60 hover:border-b-border focus:border-b-foreground/40 focus:text-foreground sm:max-w-xl [field-sizing:content]"
|
||||
className="max-w-full min-w-0 resize-none border-b-2 border-transparent bg-transparent text-sm text-muted-foreground caret-primary outline-none transition-colors placeholder:text-muted-foreground/60 hover:border-b-primary focus:border-b-primary sm:max-w-xl [field-sizing:content]"
|
||||
/>
|
||||
<div className="relative mt-0.5 flex size-4 shrink-0 items-center justify-center">
|
||||
<Loader2
|
||||
|
||||
@@ -52,7 +52,7 @@ export function SessionTitleEditor({ sessionId, initialName }: { sessionId: numb
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder={`Session #${sessionId}`}
|
||||
aria-label="Session name"
|
||||
className="max-w-full min-w-0 border-b-2 border-transparent bg-transparent font-heading text-2xl font-semibold text-foreground outline-none transition-colors placeholder:text-muted-foreground/70 hover:border-b-border focus:border-b-foreground/40 sm:max-w-xl [field-sizing:content]"
|
||||
className="max-w-full min-w-0 border-b-2 border-transparent bg-transparent font-heading text-2xl font-semibold text-foreground caret-primary outline-none transition-colors placeholder:text-muted-foreground/70 hover:border-b-primary focus:border-b-primary sm:max-w-xl [field-sizing:content]"
|
||||
/>
|
||||
<div className="relative flex size-4 shrink-0 items-center justify-center">
|
||||
<Loader2
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sexy",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user