* first attempt at stream sidebar, tasks, and events * fix folder from root * cleanup folders and events, and condense changes * cleanup and add toggle for sidebar * cleanup * fix nits
180 lines
6.1 KiB
TypeScript
180 lines
6.1 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import { useNetworks } from '@/hooks/use-networks';
|
|
import { cn, removeDuplicates } from '@/lib/utils';
|
|
import { metaKey } from '@/lib/platform';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import { generateRandomName } from '@/lib/random-name';
|
|
import { Input } from '@/components/ui/input';
|
|
import { KeyHint } from '@/components/key-hint';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
|
|
interface ConfigureContainerStepProps {
|
|
/** Drives labels and hints; the form is identical for both kinds. */
|
|
kind?: 'stream' | 'folder';
|
|
networkId: string | null;
|
|
onCancel: () => void;
|
|
onSubmit: (name: string, visibleTo: string[]) => void;
|
|
}
|
|
|
|
export function ConfigureContainerStep({
|
|
kind = 'stream',
|
|
networkId,
|
|
onCancel,
|
|
onSubmit,
|
|
}: ConfigureContainerStepProps) {
|
|
const { data: networks } = useNetworks();
|
|
const network = networks?.find((n) => n.id === networkId);
|
|
|
|
const [name, setName] = useState(() => generateRandomName());
|
|
const [everyone, setEveryone] = useState(true);
|
|
const userId = useAuthStore((s) => s.user?.id);
|
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
|
|
const members = (network?.humans ?? []).filter((h) => h.id !== userId);
|
|
|
|
const toggleMember = useCallback((id: string) => {
|
|
setSelectedIds((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const buildVisibleTo = useCallback((): string[] => {
|
|
if (everyone && networkId) return [`network:${networkId}`];
|
|
|
|
return Array.from(
|
|
removeDuplicates([...selectedIds, userId].filter(Boolean) as string[]),
|
|
).map((id) => `human:${id}`);
|
|
}, [everyone, networkId, selectedIds, userId]);
|
|
|
|
const handleSubmit = useCallback(() => {
|
|
if (!name.trim() || !networkId) return;
|
|
onSubmit(name.trim(), buildVisibleTo());
|
|
}, [name, networkId, onSubmit, buildVisibleTo]);
|
|
|
|
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();
|
|
}
|
|
return;
|
|
}
|
|
},
|
|
[onCancel, handleSubmit],
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className="absolute inset-0 z-50 flex flex-col bg-black/90 pt-16"
|
|
onKeyDown={handleKeyDown}
|
|
tabIndex={-1}
|
|
>
|
|
<div className="mx-auto w-full max-w-sm space-y-4 px-6">
|
|
<div>
|
|
<Label className="mb-1 text-xs text-white/50">
|
|
{kind === 'folder' ? 'Folder name' : 'Stream name'}
|
|
</Label>
|
|
<Input
|
|
type="text"
|
|
autoFocus
|
|
value={name}
|
|
onChange={(e) => {
|
|
setName(e.target.value);
|
|
}}
|
|
placeholder="Give it a name..."
|
|
className="border-white/10 bg-white/5 text-white placeholder-white/30 focus-visible:border-white/30 focus-visible:ring-0"
|
|
/>
|
|
</div>
|
|
|
|
{/* Visibility */}
|
|
<div>
|
|
<Label className="mb-1 text-xs text-white/50">Visible to</Label>
|
|
<div className="rounded-md border border-white/10">
|
|
{/* Everyone in network */}
|
|
<div
|
|
role="button"
|
|
onClick={() => setEveryone((prev) => !prev)}
|
|
className={cn(
|
|
'flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors',
|
|
'text-white/70 hover:bg-white/5',
|
|
)}
|
|
>
|
|
<Checkbox
|
|
checked={everyone}
|
|
onCheckedChange={(checked) => setEveryone(checked === true)}
|
|
tabIndex={-1}
|
|
className="pointer-events-none"
|
|
/>
|
|
<span className="font-medium">Everyone in network</span>
|
|
</div>
|
|
|
|
{/* Per-member selection */}
|
|
{!everyone && members.length > 0 && (
|
|
<ScrollArea className="max-h-48">
|
|
<div className="space-y-0.5 p-1">
|
|
{members.map((member) => {
|
|
const isSelected = selectedIds.has(member.id);
|
|
const initials = member.email_prefix
|
|
.slice(0, 2)
|
|
.toUpperCase();
|
|
|
|
return (
|
|
<div
|
|
key={member.id}
|
|
role="button"
|
|
onClick={() => toggleMember(member.id)}
|
|
className={cn(
|
|
'flex w-full cursor-pointer items-center gap-2.5 rounded px-3 py-1.5 text-left text-sm transition-colors',
|
|
'text-white/70 hover:bg-white/5',
|
|
)}
|
|
>
|
|
<Checkbox
|
|
checked={isSelected}
|
|
tabIndex={-1}
|
|
className="pointer-events-none"
|
|
/>
|
|
<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>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Keyboard hints */}
|
|
<div className="absolute bottom-8 left-0 right-0 flex items-center justify-center gap-4 text-sm text-white/50">
|
|
<KeyHint keys="Esc" onClick={onCancel} title="Cancel (or press Esc)">
|
|
cancel
|
|
</KeyHint>
|
|
<KeyHint
|
|
keys={`${metaKey}+Enter`}
|
|
onClick={handleSubmit}
|
|
title={`Create ${kind} (or press ${metaKey}+Enter)`}
|
|
>
|
|
create
|
|
</KeyHint>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|