atproto explorer pdsls.dev
atproto tool
1import { Handle } from "@atcute/lexicons"; 2import { Meta, MetaProvider } from "@solidjs/meta"; 3import { A, RouteSectionProps, useLocation, useNavigate } from "@solidjs/router"; 4import { createEffect, createSignal, ErrorBoundary, onMount, Show, Suspense } from "solid-js"; 5import { AccountManager } from "./components/account.jsx"; 6import { RecordEditor } from "./components/create.jsx"; 7import { DropdownMenu, MenuProvider, NavMenu } from "./components/dropdown.jsx"; 8import { agent } from "./components/login.jsx"; 9import { NavBar } from "./components/navbar.jsx"; 10import { Search, SearchButton, showSearch } from "./components/search.jsx"; 11import { themeEvent, ThemeSelection } from "./components/theme.jsx"; 12import { resolveHandle } from "./utils/api.js"; 13 14export const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1; 15 16export const [notif, setNotif] = createSignal<{ 17 show: boolean; 18 icon?: string; 19 text?: string; 20}>({ show: false }); 21 22const headers: Record<string, string> = { 23 "did:plc:ia76kvnndjutgedggx2ibrem": "bunny.jpg", 24 "did:plc:oisofpd7lj26yvgiivf3lxsi": "puppy.jpg", 25 "did:plc:vwzwgnygau7ed7b7wt5ux7y2": "water.webp", 26 "did:plc:uu5axsmbm2or2dngy4gwchec": "city.webp", 27 "did:plc:aokggmp5jzj4nc5jifhiplqc": "bridge.jpg", 28 "did:plc:bnqkww7bjxaacajzvu5gswdf": "forest.jpg", 29 "did:plc:p2cp5gopk7mgjegy6wadk3ep": "aurora.jpg", 30 "did:plc:ucaezectmpny7l42baeyooxi": "almaty.webp", 31}; 32 33const Layout = (props: RouteSectionProps<unknown>) => { 34 const location = useLocation(); 35 const navigate = useNavigate(); 36 let timeout: number; 37 38 if (location.search.includes("hrt=true")) localStorage.setItem("hrt", "true"); 39 else if (location.search.includes("hrt=false")) localStorage.setItem("hrt", "false"); 40 if (location.search.includes("sailor=true")) localStorage.setItem("sailor", "true"); 41 else if (location.search.includes("sailor=false")) localStorage.setItem("sailor", "false"); 42 43 createEffect(async () => { 44 if (props.params.repo && !props.params.repo.startsWith("did:")) { 45 const did = await resolveHandle(props.params.repo as Handle); 46 navigate(location.pathname.replace(props.params.repo, did)); 47 } 48 }); 49 50 createEffect(() => { 51 if (notif().show) { 52 clearTimeout(timeout); 53 timeout = setTimeout(() => setNotif({ show: false }), 3000); 54 } 55 }); 56 57 onMount(() => { 58 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent); 59 60 if (localStorage.getItem("sailor") === "true") { 61 const style = document.createElement("style"); 62 style.textContent = ` 63 html, * { 64 cursor: url(/cursor.cur), pointer; 65 } 66 67 .star { 68 position: fixed; 69 pointer-events: none; 70 z-index: 9999; 71 font-size: 20px; 72 animation: sparkle 0.8s ease-out forwards; 73 } 74 75 @keyframes sparkle { 76 0% { 77 opacity: 1; 78 transform: translate(0, 0) rotate(var(--ttheta1)) scale(1); 79 } 80 100% { 81 opacity: 0; 82 transform: translate(var(--tx), var(--ty)) rotate(var(--ttheta2)) scale(0); 83 } 84 } 85 `; 86 document.head.appendChild(style); 87 88 let lastTime = 0; 89 const throttleDelay = 30; 90 91 document.addEventListener("mousemove", (e) => { 92 const now = Date.now(); 93 if (now - lastTime < throttleDelay) return; 94 lastTime = now; 95 96 const star = document.createElement("div"); 97 star.className = "star"; 98 star.textContent = "✨"; 99 star.style.left = e.clientX + "px"; 100 star.style.top = e.clientY + "px"; 101 102 const tx = (Math.random() - 0.5) * 50; 103 const ty = (Math.random() - 0.5) * 50; 104 const ttheta1 = Math.random() * 360; 105 const ttheta2 = ttheta1 + (Math.random() - 0.5) * 540; 106 star.style.setProperty("--tx", tx + "px"); 107 star.style.setProperty("--ty", ty + "px"); 108 star.style.setProperty("--ttheta1", ttheta1 + "deg"); 109 star.style.setProperty("--ttheta2", ttheta2 + "deg"); 110 111 document.body.appendChild(star); 112 113 setTimeout(() => star.remove(), 800); 114 }); 115 } 116 }); 117 118 return ( 119 <div 120 id="main" 121 class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4 text-neutral-900 dark:text-neutral-200" 122 > 123 <MetaProvider> 124 <Show when={location.pathname !== "/"}> 125 <Meta name="robots" content="noindex, nofollow" /> 126 </Show> 127 </MetaProvider> 128 <header 129 class={`dark:shadow-dark-700 dark:bg-dark-300 mb-3 flex w-full items-center justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 bg-size-[95%] bg-right bg-no-repeat p-3 shadow-xs [--header-bg:#fafafa] dark:border-neutral-700 dark:[--header-bg:#2d2d2d] ${localStorage.getItem("hrt") === "true" ? "bg-[linear-gradient(to_left,transparent_10%,var(--header-bg)_85%),linear-gradient(to_bottom,#5BCEFA90_0%,#5BCEFA90_20%,#F5A9B890_20%,#F5A9B890_40%,#FFFFFF90_40%,#FFFFFF90_60%,#F5A9B890_60%,#F5A9B890_80%,#5BCEFA90_80%,#5BCEFA90_100%)]" : ""}`} 130 style={{ 131 "background-image": 132 props.params.repo in headers ? 133 `linear-gradient(to left, transparent 10%, var(--header-bg) 85%), url(/headers/${headers[props.params.repo]})` 134 : undefined, 135 }} 136 > 137 <A 138 href="/" 139 style='font-feature-settings: "cv05"' 140 class="flex items-center gap-1 text-xl font-semibold" 141 > 142 <span class="iconify tabler--binary-tree-filled text-[#76c4e5]"></span> 143 <span>PDSls</span> 144 </A> 145 <div class="dark:bg-dark-300/60 relative flex items-center gap-1 rounded-lg bg-neutral-50/60"> 146 <Show when={location.pathname !== "/"}> 147 <SearchButton /> 148 </Show> 149 <Show when={agent()}> 150 <RecordEditor create={true} /> 151 </Show> 152 <AccountManager /> 153 <MenuProvider> 154 <DropdownMenu 155 icon="lucide--menu text-xl" 156 buttonClass="rounded-lg p-1" 157 menuClass="top-10 p-3 text-sm" 158 > 159 <NavMenu href="/jetstream" label="Jetstream" /> 160 <NavMenu href="/firehose" label="Firehose" /> 161 <NavMenu href="/settings" label="Settings" /> 162 <NavMenu 163 href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz" 164 label="Bluesky" 165 newTab 166 external 167 /> 168 <NavMenu 169 href="https://tangled.org/@pdsls.dev/pdsls/" 170 label="Source" 171 newTab 172 external 173 /> 174 <ThemeSelection /> 175 </DropdownMenu> 176 </MenuProvider> 177 </div> 178 </header> 179 <div class="flex w-full flex-col items-center gap-3 text-pretty"> 180 <Show when={showSearch() || location.pathname === "/"}> 181 <Search /> 182 </Show> 183 <Show when={props.params.pds}> 184 <NavBar params={props.params} /> 185 </Show> 186 <Show keyed when={location.pathname}> 187 <ErrorBoundary 188 fallback={(err) => <div class="mt-3 wrap-break-word">Error: {err.message}</div>} 189 > 190 <Suspense 191 fallback={ 192 <span class="iconify lucide--loader-circle mt-3 animate-spin text-xl"></span> 193 } 194 > 195 {props.children} 196 </Suspense> 197 </ErrorBoundary> 198 </Show> 199 </div> 200 <Show when={notif().show}> 201 <button 202 class="dark:shadow-dark-700 dark:bg-dark-100 fixed bottom-10 z-50 flex items-center rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 shadow-md dark:border-neutral-700" 203 onClick={() => setNotif({ show: false })} 204 > 205 <span class={`iconify ${notif().icon} mr-1`}></span> 206 {notif().text} 207 </button> 208 </Show> 209 </div> 210 ); 211}; 212 213export { Layout };