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), { replace: true }); 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 id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4"> 108 <MetaProvider> 109 <Show when={location.pathname !== "/"}> 110 <Meta name="robots" content="noindex, nofollow" /> 111 </Show> 112 </MetaProvider> 113 <header 114 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%)]" : ""}`} 115 style={{ 116 "background-image": 117 props.params.repo && props.params.repo in headers ? 118 `linear-gradient(to left, transparent 10%, var(--header-bg) 85%), url(/headers/${headers[props.params.repo]})` 119 : undefined, 120 }} 121 > 122 <A 123 href="/" 124 style='font-feature-settings: "cv05"' 125 class="flex items-center gap-1 text-xl font-semibold" 126 > 127 <span class="iconify tabler--binary-tree-filled text-[#76c4e5]"></span> 128 <span>PDSls</span> 129 </A> 130 <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"> 131 <Show when={location.pathname !== "/"}> 132 <SearchButton /> 133 </Show> 134 <Show when={agent()}> 135 <RecordEditor create={true} /> 136 </Show> 137 <AccountManager /> 138 <MenuProvider> 139 <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5"> 140 <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 141 <NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" /> 142 <NavMenu href="/labels" label="Labels" icon="lucide--tags" /> 143 <NavMenu href="/settings" label="Settings" icon="lucide--settings" /> 144 <MenuSeparator /> 145 <NavMenu 146 href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz" 147 label="Bluesky" 148 icon="simple-icons--bluesky text-[#0085ff]" 149 newTab 150 /> 151 <NavMenu 152 href="https://tangled.org/@pdsls.dev/pdsls/" 153 label="Source" 154 icon="lucide--code" 155 newTab 156 /> 157 </DropdownMenu> 158 </MenuProvider> 159 </div> 160 </header> 161 <div class="flex w-full flex-col items-center gap-3 text-pretty"> 162 <Show when={showSearch() || location.pathname === "/"}> 163 <Search /> 164 </Show> 165 <Show when={props.params.pds}> 166 <NavBar params={props.params} /> 167 </Show> 168 <Show keyed when={location.pathname}> 169 <ErrorBoundary 170 fallback={(err) => <div class="mt-3 wrap-anywhere">Error: {err.message}</div>} 171 > 172 <Suspense 173 fallback={ 174 <span class="iconify lucide--loader-circle mt-3 animate-spin text-xl"></span> 175 } 176 > 177 {props.children} 178 </Suspense> 179 </ErrorBoundary> 180 </Show> 181 </div> 182 <NotificationContainer /> 183 </div> 184 ); 185}; 186 187export { Layout };