A personal media tracker built on the AT Protocol opnshelf.xyz
at main 170 lines 5.0 kB view raw
1import { 2 listsControllerCreateListMutation, 3 listsControllerGetUserListsQueryKey, 4} from "@opnshelf/api"; 5import { usePostHog } from "@posthog/react"; 6import { useMutation, useQueryClient } from "@tanstack/react-query"; 7import { ListPlus } from "lucide-react"; 8import { useId, useState } from "react"; 9import { useTheme } from "@/components/theme-provider"; 10import { 11 Dialog, 12 DialogContent, 13 DialogDescription, 14 DialogHeader, 15 DialogTitle, 16 DialogTrigger, 17} from "@/components/ui/dialog"; 18import { M3Button } from "@/components/ui/m3-button"; 19import { M3TextField } from "@/components/ui/m3-text-field"; 20import { cn } from "@/lib/utils"; 21 22type CreateListDialogProps = { 23 triggerClassName?: string; 24}; 25 26export function CreateListDialog({ triggerClassName }: CreateListDialogProps) { 27 const [open, setOpen] = useState(false); 28 const [name, setName] = useState(""); 29 const [description, setDescription] = useState(""); 30 const [isDescriptionFocused, setIsDescriptionFocused] = useState(false); 31 const queryClient = useQueryClient(); 32 const id = useId(); 33 const { seedColor } = useTheme(); 34 const posthog = usePostHog(); 35 36 const createListMutation = useMutation({ 37 mutationKey: ["lists", "create"], 38 ...listsControllerCreateListMutation(), 39 onSuccess: (data) => { 40 queryClient.invalidateQueries({ 41 queryKey: listsControllerGetUserListsQueryKey(), 42 }); 43 posthog.capture("list_created", { 44 list_name: name.trim(), 45 has_description: !!description.trim(), 46 list_id: (data as { id?: string })?.id, 47 }); 48 setOpen(false); 49 setName(""); 50 setDescription(""); 51 }, 52 }); 53 54 const handleSubmit = (e: React.FormEvent) => { 55 e.preventDefault(); 56 if (!name.trim()) return; 57 58 createListMutation.mutate({ 59 body: { 60 name: name.trim(), 61 description: description.trim() || undefined, 62 }, 63 }); 64 }; 65 66 return ( 67 <Dialog open={open} onOpenChange={setOpen}> 68 <DialogTrigger asChild> 69 <M3Button 70 variant="filled" 71 className={cn("gap-2 ml-auto", triggerClassName)} 72 > 73 <ListPlus className="size-4" /> 74 Create List 75 </M3Button> 76 </DialogTrigger> 77 <DialogContent 78 style={{ 79 backgroundColor: "var(--md-sys-color-surface-container)", 80 borderColor: "var(--md-sys-color-outline)", 81 color: "var(--md-sys-color-on-surface)", 82 }} 83 > 84 <DialogHeader> 85 <DialogTitle 86 className="md-headline-small" 87 style={{ color: "var(--md-sys-color-on-surface)" }} 88 > 89 Create New List 90 </DialogTitle> 91 <DialogDescription 92 className="md-body-medium" 93 style={{ color: "var(--md-sys-color-on-surface-variant)" }} 94 > 95 Create a custom list to organize your movies. 96 </DialogDescription> 97 </DialogHeader> 98 <form onSubmit={handleSubmit} className="space-y-4"> 99 <div className="space-y-2"> 100 <M3TextField 101 id={`${id}-name`} 102 label="Name" 103 placeholder="My Awesome List" 104 value={name} 105 onChange={(e) => setName(e.target.value)} 106 required 107 maxLength={100} 108 variant="outlined" 109 /> 110 </div> 111 <div className="space-y-2"> 112 <div 113 className="relative rounded-(--md-sys-shape-corner-extra-small) border bg-transparent transition-all duration-200" 114 style={{ 115 borderColor: isDescriptionFocused 116 ? "var(--md-sys-color-primary)" 117 : "var(--md-sys-color-outline)", 118 borderWidth: isDescriptionFocused ? 2 : 1, 119 }} 120 > 121 <label 122 htmlFor={`${id}-description`} 123 className="absolute left-4 top-0 -translate-y-1/2 px-1 md-label-small pointer-events-none" 124 style={{ 125 backgroundColor: "var(--md-sys-color-surface)", 126 color: isDescriptionFocused 127 ? "var(--md-sys-color-primary)" 128 : "var(--md-sys-color-on-surface-variant)", 129 }} 130 > 131 Description (optional) 132 </label> 133 <textarea 134 id={`${id}-description`} 135 placeholder="What's this list about?" 136 value={description} 137 onChange={(e) => setDescription(e.target.value)} 138 onFocus={() => setIsDescriptionFocused(true)} 139 onBlur={() => setIsDescriptionFocused(false)} 140 maxLength={500} 141 rows={3} 142 className="w-full resize-none bg-transparent py-4 px-4 text-(--md-sys-color-on-surface) placeholder:text-(--md-sys-color-on-surface-variant) outline-none md-body-large" 143 /> 144 </div> 145 </div> 146 <div className="flex justify-end gap-2"> 147 <M3Button 148 type="button" 149 variant="outlined" 150 onClick={() => setOpen(false)} 151 > 152 Cancel 153 </M3Button> 154 <M3Button 155 type="submit" 156 variant="filled" 157 disabled={!name.trim() || createListMutation.isPending} 158 style={{ 159 backgroundColor: seedColor, 160 color: "var(--md-sys-color-on-primary)", 161 }} 162 > 163 {createListMutation.isPending ? "Creating..." : "Create"} 164 </M3Button> 165 </div> 166 </form> 167 </DialogContent> 168 </Dialog> 169 ); 170}