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

constellation profilething

rimar1337 b279cb9c 06ea05da

Changed files
+23 -22
src
components
+23 -22
src/components/Login.tsx
··· 1 // src/components/Login.tsx 2 - import { Agent } from "@atproto/api"; 3 import React, { useEffect, useRef, useState } from "react"; 4 5 import { useAuth } from "~/providers/UnifiedAuthProvider"; 6 7 // --- 1. The Main Component (Orchestrator with `compact` prop) --- 8 export default function Login({ ··· 110 // --- 3. Helper components for layouts, forms, and UI --- 111 112 // A new component to contain the logic for the compact dropdown 113 - const CompactLoginButton = ({popup}:{popup?: boolean}) => { 114 const [showForm, setShowForm] = useState(false); 115 const formRef = useRef<HTMLDivElement>(null); 116 ··· 137 Log in 138 </button> 139 {showForm && ( 140 - <div className={`absolute ${popup ? `bottom-[calc(100%)]` :`top-full`} right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50`}> 141 <UnifiedLoginForm /> 142 </div> 143 )} ··· 268 269 // --- Profile Component (now supports a `large` prop for styling) --- 270 export const ProfileThing = ({ 271 - agent, 272 large = false, 273 }: { 274 - agent: Agent | null; 275 large?: boolean; 276 }) => { 277 - const [profile, setProfile] = useState<any>(null); 278 279 - useEffect(() => { 280 - const fetchUser = async () => { 281 - const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid; 282 - if (!did) return; 283 - try { 284 - const res = await agent!.getProfile({ actor: did }); 285 - setProfile(res.data); 286 - } catch (e) { 287 - console.error("Failed to fetch profile", e); 288 - } 289 - }; 290 - if (agent) fetchUser(); 291 - }, [agent]); 292 293 - if (!profile) { 294 return ( 295 // Skeleton loader 296 <div ··· 316 className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`} 317 > 318 <img 319 - src={profile?.avatar} 320 alt="avatar" 321 className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 322 /> ··· 329 <div 330 className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`} 331 > 332 - @{profile?.handle} 333 </div> 334 </div> 335 </div>
··· 1 // src/components/Login.tsx 2 + import AtpAgent, { Agent } from "@atproto/api"; 3 import React, { useEffect, useRef, useState } from "react"; 4 5 import { useAuth } from "~/providers/UnifiedAuthProvider"; 6 + import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery"; 7 8 // --- 1. The Main Component (Orchestrator with `compact` prop) --- 9 export default function Login({ ··· 111 // --- 3. Helper components for layouts, forms, and UI --- 112 113 // A new component to contain the logic for the compact dropdown 114 + const CompactLoginButton = ({ popup }: { popup?: boolean }) => { 115 const [showForm, setShowForm] = useState(false); 116 const formRef = useRef<HTMLDivElement>(null); 117 ··· 138 Log in 139 </button> 140 {showForm && ( 141 + <div 142 + className={`absolute ${popup ? `bottom-[calc(100%)]` : `top-full`} right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50`} 143 + > 144 <UnifiedLoginForm /> 145 </div> 146 )} ··· 271 272 // --- Profile Component (now supports a `large` prop for styling) --- 273 export const ProfileThing = ({ 274 + agent: _unused, 275 large = false, 276 }: { 277 + agent?: Agent | null; 278 large?: boolean; 279 }) => { 280 + const { agent } = useAuth(); 281 + const did = ((agent as AtpAgent).session?.did ?? (agent as AtpAgent)?.assertDid ?? agent?.did) as 282 + | string 283 + | undefined; 284 + const { data: identity } = useQueryIdentity(did); 285 + const { data: profiledata } = useQueryProfile(`at://${did}/app.bsky.actor.profile/self`); 286 + const profile = profiledata?.value; 287 288 + function getAvatarUrl(p: typeof profile) { 289 + const link = p?.avatar?.ref?.["$link"]; 290 + if (!link || !did) return null; 291 + return `https://cdn.bsky.app/img/avatar/plain/${did}/${link}@jpeg`; 292 + } 293 294 + if (!profiledata) { 295 return ( 296 // Skeleton loader 297 <div ··· 317 className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`} 318 > 319 <img 320 + src={getAvatarUrl(profile) ?? undefined} 321 alt="avatar" 322 className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 323 /> ··· 330 <div 331 className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`} 332 > 333 + @{identity?.handle} 334 </div> 335 </div> 336 </div>