197 lines
6.0 KiB
TypeScript
197 lines
6.0 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from "react";
|
|
import { useNetworks } from "@/hooks/use-networks";
|
|
import { cn } from "@/lib/utils";
|
|
import { Check } from "lucide-react";
|
|
|
|
interface ConfigureStreamStepProps {
|
|
networkId: string | null;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export function ConfigureStreamStep({
|
|
networkId,
|
|
onCancel,
|
|
}: ConfigureStreamStepProps) {
|
|
|
|
const { data: networks } = useNetworks();
|
|
const network = networks?.find((n) => n.id === networkId);
|
|
const members = network?.humans ?? [];
|
|
|
|
const [name, setName] = useState("");
|
|
const [selectedEmails, setSelectedEmails] = useState<Set<string>>(new Set());
|
|
// -1 = name input is focused, 0+ = member list index
|
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
const nameRef = useRef<HTMLInputElement>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
nameRef.current?.focus();
|
|
}, []);
|
|
|
|
// Return focus to the name input when navigating back up
|
|
useEffect(() => {
|
|
if (focusedIndex === -1) {
|
|
nameRef.current?.focus();
|
|
} else {
|
|
// Blur the input so arrow keys don't move the cursor
|
|
nameRef.current?.blur();
|
|
containerRef.current?.focus();
|
|
}
|
|
}, [focusedIndex]);
|
|
|
|
const toggleMember = useCallback((email: string) => {
|
|
setSelectedEmails((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(email)) next.delete(email);
|
|
else next.add(email);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
if (!name.trim() || !networkId) return;
|
|
// TODO: create stream particle, then attach recorded/text content
|
|
onCancel();
|
|
}, [name, networkId, onCancel]);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: React.KeyboardEvent) => {
|
|
switch (e.key) {
|
|
case "Escape":
|
|
e.preventDefault();
|
|
onCancel();
|
|
return;
|
|
|
|
case "Enter":
|
|
if (e.metaKey || e.ctrlKey) {
|
|
e.preventDefault();
|
|
handleSubmit();
|
|
} else if (focusedIndex === -1 && name.trim() && members.length > 0) {
|
|
// Enter in name input → move to member list
|
|
e.preventDefault();
|
|
setFocusedIndex(0);
|
|
}
|
|
return;
|
|
|
|
case "ArrowDown":
|
|
e.preventDefault();
|
|
setFocusedIndex((i) => Math.min(i + 1, members.length - 1));
|
|
return;
|
|
|
|
case "ArrowUp":
|
|
e.preventDefault();
|
|
setFocusedIndex((i) => Math.max(i - 1, -1));
|
|
return;
|
|
|
|
case " ":
|
|
if (focusedIndex >= 0) {
|
|
e.preventDefault();
|
|
toggleMember(members[focusedIndex].email);
|
|
}
|
|
return;
|
|
}
|
|
},
|
|
[onCancel, handleSubmit, focusedIndex, members, name, toggleMember],
|
|
);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className="absolute inset-0 z-50 flex items-center justify-center bg-black/90"
|
|
onKeyDown={handleKeyDown}
|
|
tabIndex={-1}
|
|
>
|
|
<div className="w-full max-w-sm space-y-4 px-6">
|
|
{/* Stream name */}
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-white/50">
|
|
Stream name
|
|
</label>
|
|
<input
|
|
ref={nameRef}
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => {
|
|
setName(e.target.value);
|
|
setFocusedIndex(-1);
|
|
}}
|
|
onFocus={() => setFocusedIndex(-1)}
|
|
placeholder="Give it a name..."
|
|
className="w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-sm text-white placeholder-white/30 outline-none focus:border-white/30"
|
|
/>
|
|
</div>
|
|
|
|
{/* Member selection */}
|
|
{members.length > 0 && (
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-white/50">
|
|
Visible to
|
|
</label>
|
|
<div className="space-y-0.5 rounded-md border border-white/10 p-1">
|
|
{members.map((member, index) => {
|
|
const isSelected = selectedEmails.has(member.email);
|
|
const isFocused = focusedIndex === index;
|
|
const initials = member.email_prefix
|
|
.slice(0, 2)
|
|
.toUpperCase();
|
|
|
|
return (
|
|
<button
|
|
key={member.email}
|
|
type="button"
|
|
onClick={() => toggleMember(member.email)}
|
|
className={cn(
|
|
"flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm transition-colors",
|
|
isFocused
|
|
? "bg-white/10 text-white"
|
|
: "text-white/70 hover:bg-white/5",
|
|
)}
|
|
>
|
|
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-white/10 text-[10px] font-medium">
|
|
{initials}
|
|
</span>
|
|
<span className="flex-1 truncate">
|
|
{member.email_prefix}
|
|
</span>
|
|
{isSelected && (
|
|
<Check className="h-3.5 w-3.5 text-white/70" />
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Keyboard hints */}
|
|
<div className="absolute bottom-8 flex items-center gap-4 text-sm text-white/50">
|
|
<span>
|
|
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
|
Esc
|
|
</kbd>{" "}
|
|
cancel
|
|
</span>
|
|
<span>
|
|
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
|
↑↓
|
|
</kbd>{" "}
|
|
navigate
|
|
</span>
|
|
<span>
|
|
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
|
Space
|
|
</kbd>{" "}
|
|
toggle
|
|
</span>
|
|
<span>
|
|
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
|
⌘+Enter
|
|
</kbd>{" "}
|
|
create
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|