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