Highly ambitious ATProtocol AppView service and sdks
at main 195 lines 5.8 kB view raw
1import { useState } from "react"; 2import { graphql, useMutation } from "react-relay"; 3import { useNavigate } from "react-router-dom"; 4import { Dialog } from "./Dialog.tsx"; 5import { FormControl } from "./FormControl.tsx"; 6import { Input } from "./Input.tsx"; 7import { Button } from "./Button.tsx"; 8import { getRkey } from "../utils.ts"; 9import type { CreateSliceDialogMutation } from "../__generated__/CreateSliceDialogMutation.graphql.ts"; 10 11interface CreateSliceDialogProps { 12 open: boolean; 13 onClose: () => void; 14} 15 16export function CreateSliceDialog({ open, onClose }: CreateSliceDialogProps) { 17 const navigate = useNavigate(); 18 const [name, setName] = useState(""); 19 const [domain, setDomain] = useState(""); 20 const [error, setError] = useState(""); 21 22 const [commitMutation, isMutationInFlight] = 23 useMutation<CreateSliceDialogMutation>( 24 graphql` 25 mutation CreateSliceDialogMutation($input: NetworkSlicesSliceInput!) { 26 createNetworkSlicesSlice(input: $input) { 27 id 28 uri 29 cid 30 name 31 domain 32 createdAt 33 actorHandle 34 } 35 } 36 ` 37 ); 38 39 const handleSubmit = (e: React.FormEvent) => { 40 e.preventDefault(); 41 setError(""); 42 43 if (!name.trim()) { 44 setError("Slice name is required"); 45 return; 46 } 47 48 if (!domain.trim()) { 49 setError("Domain is required"); 50 return; 51 } 52 53 if (domain.toLowerCase().includes("app.bsky")) { 54 setError("app.bsky domains are not allowed"); 55 return; 56 } 57 58 commitMutation({ 59 variables: { 60 input: { 61 name: name.trim(), 62 domain: domain.trim(), 63 createdAt: new Date().toISOString(), 64 }, 65 }, 66 onCompleted: (response) => { 67 setName(""); 68 setDomain(""); 69 onClose(); 70 // Navigate to the new slice page 71 const slice = response.createNetworkSlicesSlice; 72 const actorHandle = slice.actorHandle; 73 const rkey = getRkey(slice.uri); 74 if (actorHandle && rkey) { 75 navigate(`/profile/${actorHandle}/slice/${rkey}`); 76 } else { 77 navigate("/"); 78 } 79 }, 80 onError: (err) => { 81 setError(err.message || "Failed to create slice"); 82 }, 83 updater: (store) => { 84 const newSlice = store.getRootField("createNetworkSlicesSlice"); 85 if (!newSlice) return; 86 87 const actorHandle = newSlice.getValue("actorHandle"); 88 if (!actorHandle) return; 89 90 // Update Home page connection (all slices) 91 const root = store.getRoot(); 92 const homeConnection = root.getLinkedRecord("networkSlicesSlices", { 93 first: 100, 94 sortBy: [{ field: "createdAt", direction: "desc" }], 95 }); 96 97 if (homeConnection) { 98 const homeEdges = homeConnection.getLinkedRecords("edges") || []; 99 const newEdge = store.create( 100 `client:newEdge:${newSlice.getDataID()}`, 101 "NetworkSlicesSliceEdge" 102 ); 103 newEdge.setLinkedRecord(newSlice, "node"); 104 homeConnection.setLinkedRecords([newEdge, ...homeEdges], "edges"); 105 } 106 107 // Update Profile page connection (filtered by actorHandle) 108 const profileConnection = root.getLinkedRecord("networkSlicesSlices", { 109 first: 100, 110 where: { actorHandle: { eq: actorHandle } }, 111 sortBy: [{ field: "createdAt", direction: "desc" }], 112 }); 113 114 if (profileConnection) { 115 const profileEdges = profileConnection.getLinkedRecords("edges") || []; 116 const newProfileEdge = store.create( 117 `client:newProfileEdge:${newSlice.getDataID()}`, 118 "NetworkSlicesSliceEdge" 119 ); 120 newProfileEdge.setLinkedRecord(newSlice, "node"); 121 profileConnection.setLinkedRecords([newProfileEdge, ...profileEdges], "edges"); 122 } 123 }, 124 }); 125 }; 126 127 const handleClose = () => { 128 setName(""); 129 setDomain(""); 130 setError(""); 131 onClose(); 132 }; 133 134 return ( 135 <Dialog open={open} onClose={handleClose} title="Create New Slice"> 136 {error && ( 137 <div className="mb-4 p-3 bg-red-900/20 border border-red-800 text-red-300 rounded text-sm"> 138 {error} 139 </div> 140 )} 141 142 <form className="space-y-4" noValidate> 143 <FormControl label="Slice Name" htmlFor="name"> 144 <Input 145 id="name" 146 name="name" 147 type="text" 148 value={name} 149 onChange={(e) => setName(e.target.value)} 150 placeholder="Enter slice name" 151 disabled={isMutationInFlight} 152 /> 153 </FormControl> 154 155 <FormControl label="Primary Domain" htmlFor="domain"> 156 <Input 157 id="domain" 158 name="domain" 159 type="text" 160 value={domain} 161 onChange={(e) => setDomain(e.target.value)} 162 placeholder="e.g. social.grain" 163 disabled={isMutationInFlight} 164 /> 165 <p className="mt-1 text-xs text-zinc-500"> 166 Primary namespace for this slice's collections 167 </p> 168 </FormControl> 169 170 <div className="flex justify-end gap-3 pt-4"> 171 <Button 172 type="button" 173 variant="default" 174 onClick={handleClose} 175 disabled={isMutationInFlight} 176 > 177 Cancel 178 </Button> 179 <Button 180 type="button" 181 variant="primary" 182 onClick={(e) => { 183 e.preventDefault(); 184 e.stopPropagation(); 185 handleSubmit(e); 186 }} 187 disabled={isMutationInFlight} 188 > 189 {isMutationInFlight ? "Creating..." : "Create Slice"} 190 </Button> 191 </div> 192 </form> 193 </Dialog> 194 ); 195}