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 { SessionTimelineChart } from "@/components/sessions/SessionTimelineChart";
|
||||||
import { SessionTitleEditor } from "@/components/sessions/SessionTitleEditor";
|
import { SessionTitleEditor } from "@/components/sessions/SessionTitleEditor";
|
||||||
import { SessionDescriptionEditor } from "@/components/sessions/SessionDescriptionEditor";
|
import { SessionDescriptionEditor } from "@/components/sessions/SessionDescriptionEditor";
|
||||||
|
import { SessionDeleteButton } from "@/components/sessions/SessionDeleteButton";
|
||||||
import { STATUS_VARIANT } from "@/components/sessions/session-status";
|
import { STATUS_VARIANT } from "@/components/sessions/session-status";
|
||||||
import { getSessionDetail, getSessionName } from "@/lib/db/queries/sessions";
|
import { getSessionDetail, getSessionName } from "@/lib/db/queries/sessions";
|
||||||
import { getSessionTimeline } from "@/lib/db/queries/stats";
|
import { getSessionTimeline } from "@/lib/db/queries/stats";
|
||||||
@@ -65,6 +66,7 @@ export default async function SessionDetailPage({ params }: { params: Promise<{
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<SessionDeleteButton sessionId={detail.session.id} sessionName={detail.session.name} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export function ButtplugConsole() {
|
|||||||
|
|
||||||
{Object.keys(devices).length === 0 ? (
|
{Object.keys(devices).length === 0 ? (
|
||||||
<Card className="bp-glass">
|
<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
|
No devices connected yet. Scan to discover nearby toys, then select one from your browser's
|
||||||
pairing prompt.
|
pairing prompt.
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Check, Loader2 } from "lucide-react";
|
import { Check, Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -55,23 +54,23 @@ function DeviceNameCell({ device }: { device: DeviceRow }) {
|
|||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-48">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<input
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
className="h-8 pr-8"
|
|
||||||
aria-label="Device display name"
|
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
|
<Loader2
|
||||||
className={cn(
|
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",
|
status === "saving" ? "opacity-100" : "opacity-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute size-3.5 text-primary transition-opacity",
|
"absolute size-4 text-primary transition-opacity",
|
||||||
status === "saved" ? "opacity-100" : "opacity-0",
|
status === "saved" ? "opacity-100" : "opacity-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const LINKS = [
|
|||||||
{ href: "/", label: "Dashboard" },
|
{ href: "/", label: "Dashboard" },
|
||||||
{ href: "/control", label: "Control" },
|
{ href: "/control", label: "Control" },
|
||||||
{ href: "/sessions", label: "Sessions" },
|
{ href: "/sessions", label: "Sessions" },
|
||||||
{ href: "/stats", label: "Stats" },
|
|
||||||
{ href: "/devices", label: "Devices" },
|
{ href: "/devices", label: "Devices" },
|
||||||
|
{ href: "/stats", label: "Stats" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function isActive(pathname: string, href: string): boolean {
|
function isActive(pathname: string, href: string): boolean {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export function ReplayPlayer({ sessionId }: { sessionId: number }) {
|
|||||||
|
|
||||||
{connectedDevices.length === 0 && (
|
{connectedDevices.length === 0 && (
|
||||||
<Card className="bp-glass">
|
<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
|
No devices connected yet. Scan to discover nearby toys, then match them to this session's
|
||||||
original device slots.
|
original device slots.
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -173,7 +173,7 @@ export function ReplayPlayer({ sessionId }: { sessionId: number }) {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Card className="bp-glass">
|
<Card className="bp-glass">
|
||||||
<CardContent className="flex flex-col gap-3 py-6">
|
<CardContent className="flex flex-col gap-3">
|
||||||
{replayTargets.length > 0 && (
|
{replayTargets.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{replayTargets.map((t) => (
|
{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..."
|
placeholder="Add a description..."
|
||||||
aria-label="Session description"
|
aria-label="Session description"
|
||||||
rows={1}
|
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">
|
<div className="relative mt-0.5 flex size-4 shrink-0 items-center justify-center">
|
||||||
<Loader2
|
<Loader2
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function SessionTitleEditor({ sessionId, initialName }: { sessionId: numb
|
|||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
placeholder={`Session #${sessionId}`}
|
placeholder={`Session #${sessionId}`}
|
||||||
aria-label="Session name"
|
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">
|
<div className="relative flex size-4 shrink-0 items-center justify-center">
|
||||||
<Loader2
|
<Loader2
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sexy",
|
"name": "sexy",
|
||||||
"version": "0.8.0",
|
"version": "0.8.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user