use drawer for stream selection on mobile in step 3

This commit is contained in:
Josh Hawkins 2025-11-10 15:24:11 -06:00
parent 42f11c71e7
commit b88bf2f08d

View File

@ -25,11 +25,17 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { LuInfo, LuExternalLink } from "react-icons/lu"; import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import { isMobile } from "react-device-detect";
import {
LuInfo,
LuExternalLink,
LuCheck,
LuChevronsUpDown,
} from "react-icons/lu";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useDocDomain } from "@/hooks/use-doc-domain"; import { useDocDomain } from "@/hooks/use-doc-domain";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Check, ChevronsUpDown } from "lucide-react";
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
@ -384,84 +390,165 @@ export default function Step3StreamConfig({
</label> </label>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
{isProbeMode && probeCandidates.length > 0 ? ( {isProbeMode && probeCandidates.length > 0 ? (
<Popover // Responsive: Popover on desktop, Drawer on mobile
open={openCombobox === stream.id} !isMobile ? (
onOpenChange={(isOpen) => { <Popover
setOpenCombobox(isOpen ? stream.id : null); open={openCombobox === stream.id}
}} onOpenChange={(isOpen) => {
> setOpenCombobox(isOpen ? stream.id : null);
<PopoverTrigger asChild> }}
<div className="min-w-0 flex-1">
<Button
variant="outline"
role="combobox"
aria-expanded={openCombobox === stream.id}
className="h-8 w-full justify-between overflow-hidden text-left"
>
<span className="truncate">
{stream.url
? stream.url
: t("cameraWizard.step3.selectStream")}
</span>
<ChevronsUpDown className="ml-2 size-6 opacity-50" />
</Button>
</div>
</PopoverTrigger>
<PopoverContent
className="w-[--radix-popover-trigger-width] p-2"
disablePortal
> >
<Command> <PopoverTrigger asChild>
<CommandInput <div className="min-w-0 flex-1">
placeholder={t( <Button
"cameraWizard.step3.searchCandidates", variant="outline"
)} role="combobox"
className="h-9" aria-expanded={openCombobox === stream.id}
/> className="h-8 w-full justify-between overflow-hidden text-left"
<CommandList> >
<CommandEmpty> <span className="truncate">
{t("cameraWizard.step3.noStreamFound")} {stream.url
</CommandEmpty> ? stream.url
<CommandGroup> : t("cameraWizard.step3.selectStream")}
{probeCandidates </span>
.filter((c) => { <LuChevronsUpDown className="ml-2 size-6 opacity-50" />
const used = getUsedUrlsExcludingStream( </Button>
stream.id, </div>
); </PopoverTrigger>
return !used.has(c); <PopoverContent
}) className="w-[--radix-popover-trigger-width] p-2"
.map((candidate) => ( disablePortal
<CommandItem >
key={candidate} <Command>
value={candidate} <CommandInput
onSelect={() => { placeholder={t(
updateStream(stream.id, { "cameraWizard.step3.searchCandidates",
url: candidate, )}
testResult: className="h-9"
candidateTests[candidate] || />
undefined, <CommandList>
userTested: <CommandEmpty>
!!candidateTests[candidate], {t("cameraWizard.step3.noStreamFound")}
}); </CommandEmpty>
setOpenCombobox(null); <CommandGroup>
}} {probeCandidates
> .filter((c) => {
<Check const used = getUsedUrlsExcludingStream(
className={cn( stream.id,
"mr-3", );
stream.url === candidate return !used.has(c);
? "opacity-100" })
: "opacity-0", .map((candidate) => (
)} <CommandItem
/> key={candidate}
{candidate} value={candidate}
</CommandItem> onSelect={() => {
))} updateStream(stream.id, {
</CommandGroup> url: candidate,
</CommandList> testResult:
</Command> candidateTests[candidate] ||
</PopoverContent> undefined,
</Popover> userTested:
!!candidateTests[candidate],
});
setOpenCombobox(null);
}}
>
<LuCheck
className={cn(
"mr-3 size-5",
stream.url === candidate
? "opacity-100"
: "opacity-0",
)}
/>
{candidate}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<Drawer
open={openCombobox === stream.id}
onOpenChange={(isOpen) =>
setOpenCombobox(isOpen ? stream.id : null)
}
>
<DrawerTrigger asChild>
<div className="min-w-0 flex-1">
<Button
variant="outline"
role="combobox"
aria-expanded={openCombobox === stream.id}
className="h-8 w-full justify-between overflow-hidden text-left"
>
<span className="truncate">
{stream.url
? stream.url
: t("cameraWizard.step3.selectStream")}
</span>
<LuChevronsUpDown className="ml-2 size-6 opacity-50" />
</Button>
</div>
</DrawerTrigger>
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl px-2">
<div className="mt-2">
<Command>
<CommandInput
placeholder={t(
"cameraWizard.step3.searchCandidates",
)}
className="h-9"
/>
<CommandList>
<CommandEmpty>
{t("cameraWizard.step3.noStreamFound")}
</CommandEmpty>
<CommandGroup>
{probeCandidates
.filter((c) => {
const used = getUsedUrlsExcludingStream(
stream.id,
);
return !used.has(c);
})
.map((candidate) => (
<CommandItem
key={candidate}
value={candidate}
onSelect={() => {
updateStream(stream.id, {
url: candidate,
testResult:
candidateTests[candidate] ||
undefined,
userTested:
!!candidateTests[candidate],
});
setOpenCombobox(null);
}}
>
<LuCheck
className={cn(
"mr-3 size-5",
stream.url === candidate
? "opacity-100"
: "opacity-0",
)}
/>
{candidate}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</div>
</DrawerContent>
</Drawer>
)
) : ( ) : (
<Input <Input
value={stream.url} value={stream.url}