an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

clickable profile hover card

rimar1337 96fdf44f b3faaa61

Changed files
+61 -41
src
components
routes
profile.$did
+1
src/components/UniversalPostRenderer.tsx
··· 1542 1542 className="rounded-md p-4 w-72 bg-gray-50 dark:bg-gray-900 shadow-lg border border-gray-300 dark:border-gray-800 animate-slide-fade z-50" 1543 1543 side={"bottom"} 1544 1544 sideOffset={5} 1545 + onClick={onProfileClick} 1545 1546 > 1546 1547 <div className="flex flex-col gap-2"> 1547 1548 <div className="flex flex-row">
+60 -41
src/routes/profile.$did/index.tsx
··· 2 2 import { useQueryClient } from "@tanstack/react-query"; 3 3 import { createFileRoute, useNavigate } from "@tanstack/react-router"; 4 4 import { useAtom } from "jotai"; 5 - import React, { type ReactNode,useEffect, useState } from "react"; 5 + import React, { type ReactNode, useEffect, useState } from "react"; 6 6 7 7 import { Header } from "~/components/Header"; 8 - import { renderTextWithFacets, UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 8 + import { 9 + renderTextWithFacets, 10 + UniversalPostRendererATURILoader, 11 + } from "~/components/UniversalPostRenderer"; 9 12 import { useAuth } from "~/providers/UnifiedAuthProvider"; 10 13 import { imgCDNAtom } from "~/utils/atoms"; 11 - import { toggleFollow, useGetFollowState, useGetOneToOneState } from "~/utils/followState"; 14 + import { 15 + toggleFollow, 16 + useGetFollowState, 17 + useGetOneToOneState, 18 + } from "~/utils/followState"; 12 19 import { 13 20 useInfiniteQueryAuthorFeed, 14 21 useQueryIdentity, ··· 172 179 <div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100"> 173 180 <div className="font-bold text-2xl">{displayName}</div> 174 181 <div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1"> 175 - <Mutual targetdidorhandle={did} /> 182 + <Mutual targetdidorhandle={did} /> 176 183 {handle} 177 184 </div> 178 185 {description && ( 179 186 <div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]"> 180 187 {/* {description} */} 181 - <RichTextRenderer key={did} description={description}/> 188 + <RichTextRenderer key={did} description={description} /> 182 189 </div> 183 190 )} 184 191 </div> ··· 222 229 ); 223 230 } 224 231 225 - export function FollowButton({targetdidorhandle}:{targetdidorhandle: string}) { 226 - const {agent} = useAuth() 227 - const {data: identity} = useQueryIdentity(targetdidorhandle); 232 + export function FollowButton({ 233 + targetdidorhandle, 234 + }: { 235 + targetdidorhandle: string; 236 + }) { 237 + const { agent } = useAuth(); 238 + const { data: identity } = useQueryIdentity(targetdidorhandle); 228 239 const queryClient = useQueryClient(); 229 240 230 241 const followRecords = useGetFollowState({ 231 242 target: identity?.did ?? targetdidorhandle, 232 243 user: agent?.did, 233 244 }); 234 - 245 + 235 246 return ( 236 247 <> 237 248 {identity?.did !== agent?.did ? ( 238 249 <> 239 250 {!(followRecords?.length && followRecords?.length > 0) ? ( 240 251 <button 241 - onClick={(e) => 242 - { 252 + onClick={(e) => { 243 253 e.stopPropagation(); 244 254 toggleFollow({ 245 255 agent: agent || undefined, 246 256 targetDid: identity?.did, 247 257 followRecords: followRecords, 248 258 queryClient: queryClient, 249 - }) 250 - } 251 - } 259 + }); 260 + }} 252 261 className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" 253 262 > 254 263 Follow 255 264 </button> 256 265 ) : ( 257 266 <button 258 - onClick={(e) => 259 - { 267 + onClick={(e) => { 260 268 e.stopPropagation(); 261 269 toggleFollow({ 262 270 agent: agent || undefined, 263 271 targetDid: identity?.did, 264 272 followRecords: followRecords, 265 273 queryClient: queryClient, 266 - }) 267 - } 268 - } 274 + }); 275 + }} 269 276 className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" 270 277 > 271 278 Unfollow ··· 281 288 ); 282 289 } 283 290 291 + export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) { 292 + const { agent } = useAuth(); 293 + const { data: identity } = useQueryIdentity(targetdidorhandle); 284 294 285 - export function Mutual({targetdidorhandle}:{targetdidorhandle: string}) { 286 - const {agent} = useAuth() 287 - const {data: identity} = useQueryIdentity(targetdidorhandle); 288 - 289 - const theyFollowYouRes = useGetOneToOneState(agent?.did ? { 290 - target: agent?.did, 291 - user: identity?.did ?? targetdidorhandle, 292 - collection: "app.bsky.graph.follow", 293 - path: ".subject" 294 - }:undefined); 295 + const theyFollowYouRes = useGetOneToOneState( 296 + agent?.did 297 + ? { 298 + target: agent?.did, 299 + user: identity?.did ?? targetdidorhandle, 300 + collection: "app.bsky.graph.follow", 301 + path: ".subject", 302 + } 303 + : undefined 304 + ); 295 305 296 306 const youFollowThemRes = useGetFollowState({ 297 307 target: identity?.did ?? targetdidorhandle, 298 308 user: agent?.did, 299 309 }); 300 310 301 - const theyFollowYou: boolean = (!!theyFollowYouRes?.length && theyFollowYouRes.length > 0) 302 - const youFollowThem: boolean = (!!youFollowThemRes?.length && youFollowThemRes.length > 0) 303 - 311 + const theyFollowYou: boolean = 312 + !!theyFollowYouRes?.length && theyFollowYouRes.length > 0; 313 + const youFollowThem: boolean = 314 + !!youFollowThemRes?.length && youFollowThemRes.length > 0; 315 + 304 316 return ( 305 317 <> 306 318 {/* if not self */} 307 319 {identity?.did !== agent?.did ? ( 308 320 <> 309 - {(theyFollowYou) ? ( 321 + {theyFollowYou ? ( 310 322 <> 311 323 {youFollowThem ? ( 312 - <div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">mutuals</div> 313 - ) : ( 314 - <div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">follows you</div> 315 - ) 316 - } 324 + <div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center"> 325 + mutuals 326 + </div> 327 + ) : ( 328 + <div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center"> 329 + follows you 330 + </div> 331 + )} 317 332 </> 318 333 ) : ( 319 334 <></> ··· 328 343 } 329 344 330 345 export function RichTextRenderer({ description }: { description: string }) { 331 - const [richDescription, setRichDescription] = useState<string | ReactNode[]>(description); 346 + const [richDescription, setRichDescription] = useState<string | ReactNode[]>( 347 + description 348 + ); 332 349 const { agent } = useAuth(); 333 350 const navigate = useNavigate(); 334 351 ··· 346 363 if (!mounted) return; 347 364 348 365 if (rt.facets) { 349 - setRichDescription(renderTextWithFacets({ text: rt.text, facets: rt.facets, navigate })); 366 + setRichDescription( 367 + renderTextWithFacets({ text: rt.text, facets: rt.facets, navigate }) 368 + ); 350 369 } else { 351 370 setRichDescription(rt.text); 352 371 } ··· 366 385 }, [description, agent, navigate]); 367 386 368 387 return <>{richDescription}</>; 369 - } 388 + }