an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app

Polls initial writes

+155 -11
+155 -11
src/components/UniversalPostRenderer.tsx
··· 1 1 import * as ATPAPI from "@atproto/api"; 2 + import { useQuery } from "@tanstack/react-query"; 2 3 import { useNavigate } from "@tanstack/react-router"; 3 4 import DOMPurify from "dompurify"; 4 5 import { useAtom } from "jotai"; ··· 16 17 } from "~/utils/atoms"; 17 18 import { useHydratedEmbed } from "~/utils/useHydrated"; 18 19 import { 20 + constructConstellationQuery, 19 21 useQueryArbitrary, 20 22 useQueryConstellation, 21 23 useQueryIdentity, ··· 2143 2145 }; 2144 2146 2145 2147 function PollEmbed({ did, rkey }: { did: string; rkey: string }) { 2148 + const { agent } = useAuth(); 2146 2149 const pollUri = `at://${did}/app.reddwarf.embed.poll/${rkey}`; 2147 2150 const { data: pollRecord, isLoading, error } = useQueryArbitrary(pollUri); 2148 2151 2152 + // Query vote counts for each option 2153 + const [constellationurl] = useAtom(constellationURLAtom); 2154 + 2155 + const { data: voteCountsA } = useQueryConstellation({ 2156 + method: "/links/count/distinct-dids", 2157 + target: pollUri, 2158 + collection: "app.reddwarf.poll.vote.a", 2159 + path: ".subject.uri", 2160 + }); 2161 + 2162 + const { data: voteCountsB } = useQueryConstellation({ 2163 + method: "/links/count/distinct-dids", 2164 + target: pollUri, 2165 + collection: "app.reddwarf.poll.vote.b", 2166 + path: ".subject.uri", 2167 + }); 2168 + 2169 + const { data: voteCountsC } = useQueryConstellation({ 2170 + method: "/links/count/distinct-dids", 2171 + target: pollUri, 2172 + collection: "app.reddwarf.poll.vote.c", 2173 + path: ".subject.uri", 2174 + }); 2175 + 2176 + const { data: voteCountsD } = useQueryConstellation({ 2177 + method: "/links/count/distinct-dids", 2178 + target: pollUri, 2179 + collection: "app.reddwarf.poll.vote.d", 2180 + path: ".subject.uri", 2181 + }); 2182 + 2183 + // Query first page of voters for each option to get PFPs 2184 + const { data: votersA } = useQuery( 2185 + constructConstellationQuery({ 2186 + constellation: constellationurl, 2187 + method: "/links", 2188 + target: pollUri, 2189 + collection: "app.reddwarf.poll.vote.a", 2190 + path: ".subject.uri", 2191 + }), 2192 + ); 2193 + 2194 + const { data: votersB } = useQuery( 2195 + constructConstellationQuery({ 2196 + constellation: constellationurl, 2197 + method: "/links", 2198 + target: pollUri, 2199 + collection: "app.reddwarf.poll.vote.b", 2200 + path: ".subject.uri", 2201 + }), 2202 + ); 2203 + 2204 + const { data: votersC } = useQuery( 2205 + constructConstellationQuery({ 2206 + constellation: constellationurl, 2207 + method: "/links", 2208 + target: pollUri, 2209 + collection: "app.reddwarf.poll.vote.c", 2210 + path: ".subject.uri", 2211 + }), 2212 + ); 2213 + 2214 + const { data: votersD } = useQuery( 2215 + constructConstellationQuery({ 2216 + constellation: constellationurl, 2217 + method: "/links", 2218 + target: pollUri, 2219 + collection: "app.reddwarf.poll.vote.d", 2220 + path: ".subject.uri", 2221 + }), 2222 + ); 2223 + 2149 2224 if (isLoading) { 2150 2225 return ( 2151 2226 <div className="animate-pulse"> ··· 2187 2262 }) 2188 2263 : null; 2189 2264 2265 + // Calculate vote counts 2266 + const voteData = [ 2267 + { 2268 + option: "a", 2269 + count: parseInt((voteCountsA as any)?.total || "0"), 2270 + voters: (votersA as any)?.linking_records || [], 2271 + }, 2272 + { 2273 + option: "b", 2274 + count: parseInt((voteCountsB as any)?.total || "0"), 2275 + voters: (votersB as any)?.linking_records || [], 2276 + }, 2277 + { 2278 + option: "c", 2279 + count: parseInt((voteCountsC as any)?.total || "0"), 2280 + voters: (votersC as any)?.linking_records || [], 2281 + }, 2282 + { 2283 + option: "d", 2284 + count: parseInt((voteCountsD as any)?.total || "0"), 2285 + voters: (votersD as any)?.linking_records || [], 2286 + }, 2287 + ].slice(0, options.length); 2288 + 2289 + const totalVotes = voteData.reduce((sum, item) => sum + item.count, 0); 2290 + 2291 + const handleVote = async (option: string) => { 2292 + if (!agent || isExpired) return; 2293 + 2294 + try { 2295 + await agent.com.atproto.repo.createRecord({ 2296 + collection: `app.reddwarf.poll.vote.${option}`, 2297 + repo: agent.assertDid, 2298 + record: { 2299 + $type: `app.reddwarf.poll.vote.${option}`, 2300 + subject: { 2301 + $type: "com.atproto.repo.strongRef", 2302 + uri: pollUri, 2303 + cid: pollRecord.cid, 2304 + }, 2305 + createdAt: new Date().toISOString(), 2306 + }, 2307 + // Let PDS generate rkey automatically 2308 + }); 2309 + } catch (error) { 2310 + console.error("Failed to vote:", error); 2311 + } 2312 + }; 2313 + 2190 2314 return ( 2191 2315 <div className="my-4"> 2192 2316 {/* Header */} ··· 2201 2325 <span className="text-sm font-normal text-gray-500 dark:text-gray-400"> 2202 2326 {poll.multiple ? "Select multiple options" : "Select one option"} 2203 2327 </span> 2328 + 2329 + {/* Total Votes */} 2330 + <span className="text-sm font-medium text-gray-600 dark:text-gray-400"> 2331 + {totalVotes} vote{totalVotes !== 1 ? "s" : ""} 2332 + </span> 2204 2333 </div> 2205 2334 2206 - {/* Options List */} 2335 + {/* Options List with Results */} 2207 2336 <div className="space-y-3"> 2208 - {options.map((optionText, index) => ( 2209 - <div 2210 - key={index} 2211 - className="flex h-12 items-center justify-start truncate rounded-lg bg-gray-100 dark:bg-gray-800 px-4 text-sm font-medium text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700" 2212 - > 2213 - <span className="truncate"> 2214 - {optionText} 2215 - </span> 2216 - </div> 2217 - ))} 2337 + {options.map((optionText, index) => { 2338 + const optionKey = ["a", "b", "c", "d"][index]; 2339 + 2340 + return ( 2341 + <div 2342 + key={index} 2343 + className={`relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${ 2344 + !isExpired 2345 + ? "bg-gray-100 dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer" 2346 + : "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700" 2347 + }`} 2348 + onClick={() => !isExpired && handleVote(optionKey)} 2349 + > 2350 + {/* Option text */} 2351 + <span className="relative z-10 text-sm font-medium text-gray-900 dark:text-gray-100 truncate"> 2352 + {optionText} 2353 + </span> 2354 + 2355 + {/* Vote count */} 2356 + <span className="relative z-10 text-sm font-medium text-gray-600 dark:text-gray-400"> 2357 + {voteData.find(v => v.option === optionKey)?.count ?? 0} 2358 + </span> 2359 + </div> 2360 + ); 2361 + })} 2218 2362 </div> 2219 2363 2220 2364 {/* Footer */}