forked from pdsls.dev/pdsls
atproto explorer

Hovering over DIDs now makes a popup appear!

5jiji.com ba943148 c141b1fb

verified
Changed files
+96 -3
src
components
+94
src/components/didlink.tsx
··· 1 + import { A } from "@solidjs/router"; 2 + import { createSignal, createEffect, Show } from "solid-js"; 3 + import { createStore } from "solid-js/store"; 4 + import { Client, CredentialManager, SuccessClientResponse } from "@atcute/client"; 5 + import { Did } from "@atcute/lexicons"; 6 + import { mainSchema } from "@atcute/bluesky/types/app/actor/getProfile"; 7 + 8 + type getProfileResult = SuccessClientResponse<mainSchema, {params: {actor: Did}}>["data"]; 9 + 10 + export const DIDLink = (props: { 11 + did: Did 12 + }) => { 13 + let [hover, setHover] = createSignal(false); 14 + const [previewHeight, setPreviewHeight] = createSignal(0); 15 + const [data, setData] = createStore<Record<Did, getProfileResult>>(); 16 + 17 + let rkeyRef!: HTMLSpanElement; 18 + let previewRef!: HTMLSpanElement; 19 + 20 + createEffect(async () => { 21 + if (hover()) { 22 + setPreviewHeight(previewRef.offsetHeight); 23 + 24 + if (data[props.did] === undefined) { 25 + let data = await getData(props.did); 26 + if (data) setData(props.did, data); 27 + } 28 + }; 29 + }); 30 + 31 + const getData = async (did: Did) => { 32 + const rpc = new Client({ 33 + handler: new CredentialManager({service: "https://public.api.bsky.app"}), 34 + }); 35 + const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } }); 36 + if (res.ok) { 37 + return res.data; 38 + } 39 + return null; 40 + } 41 + 42 + const isOverflowing = (previewHeight: number) => 43 + rkeyRef.offsetTop - window.scrollY + previewHeight + 32 > window.innerHeight; 44 + 45 + return ( 46 + <span 47 + ref={rkeyRef} 48 + onmouseover={() => setHover(true)} 49 + onmouseleave={() => setHover(false)} 50 + class="relative w-full min-w-0 items-start rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 51 + > 52 + <A class="text-blue-400 hover:underline active:underline" href={`/at://${props.did}`}> 53 + {props.did} 54 + </A> 55 + <Show when={hover()}> 56 + <span 57 + ref={previewRef} 58 + class={`dark:bg-dark-300 dark:shadow-dark-700 pointer-events-none absolute left-[50%] z-25 block max-h-80 w-max max-w-sm -translate-x-1/2 overflow-hidden rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-xs whitespace-pre-wrap shadow-md sm:max-h-112 lg:max-w-lg dark:border-neutral-700 ${isOverflowing(previewHeight()) ? "bottom-7" : "top-7"}`} 59 + > 60 + <span 61 + class="flex items-center gap-2 rounded-md p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 62 + > 63 + <Show 64 + when={data[props.did]?.avatar} 65 + fallback={<span class="size-9 iconify rounded-full lucide--user-round" />} 66 + > 67 + <img 68 + src={data[props.did].avatar?.replace("img/avatar/", "img/avatar_thumbnail/")} 69 + class="size-9 iconify rounded-full" 70 + /> 71 + </Show> 72 + 73 + <div class="flex flex-col"> 74 + <Show 75 + when={data[props.did]?.displayName} 76 + fallback={<span class="text-sm font-medium">{props.did}</span>} 77 + > 78 + <span class="text-sm font-medium">{data[props.did].displayName}</span> 79 + </Show> 80 + <Show 81 + when={data[props.did]?.handle} 82 + fallback={<span class="text-xs text-neutral-600 dark:text-neutral-400">@handle.invalid</span>} 83 + > 84 + <span class="text-xs text-neutral-600 dark:text-neutral-400"> 85 + @{data[props.did].handle} 86 + </span> 87 + </Show> 88 + </div> 89 + </span> 90 + </span> 91 + </Show> 92 + </span> 93 + ) 94 + }
+2 -3
src/components/json.tsx
··· 6 6 import { pds } from "./navbar"; 7 7 import { addNotification, removeNotification } from "./notification"; 8 8 import VideoPlayer from "./video-player"; 9 + import { DIDLink } from "./didlink.tsx"; 9 10 10 11 interface AtBlob { 11 12 $type: string; ··· 61 62 {part} 62 63 </A> 63 64 : isDid(part) ? 64 - <A class="text-blue-400 hover:underline active:underline" href={`/at://${part}`}> 65 - {part} 66 - </A> 65 + <DIDLink did={part} /> 67 66 : isNsid(part.split("#")[0]) && props.isType ? 68 67 <button 69 68 type="button"