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, ThemeSelection } 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}; 27 28const Layout = (props: RouteSectionProps<unknown>) => { 29 const location = useLocation(); 30 const navigate = useNavigate(); 31 32 if (location.search.includes("hrt=true")) localStorage.setItem("hrt", "true"); 33 else if (location.search.includes("hrt=false")) localStorage.setItem("hrt", "false"); 34 if (location.search.includes("sailor=true")) localStorage.setItem("sailor", "true"); 35 else if (location.search.includes("sailor=false")) localStorage.setItem("sailor", "false"); 36 37 createEffect(async () => { 38 if (props.params.repo && !props.params.repo.startsWith("did:")) { 39 const did = await resolveHandle(props.params.repo as Handle); 40 navigate(location.pathname.replace(props.params.repo, did)); 41 } 42 }); 43 44 onMount(() => { 45 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent); 46 47 if (localStorage.getItem("sailor") === "true") { 48 const style = document.createElement("style"); 49 style.textContent = ` 50 html, * { 51 cursor: url(/cursor.cur), pointer; 52 } 53 54 .star { 55 position: fixed; 56 pointer-events: none; 57 z-index: 9999; 58 font-size: 20px; 59 animation: sparkle 0.8s ease-out forwards; 60 } 61 62 @keyframes sparkle { 63 0% { 64 opacity: 1; 65 transform: translate(0, 0) rotate(var(--ttheta1)) scale(1); 66 } 67 100% { 68 opacity: 0; 69 transform: translate(var(--tx), var(--ty)) rotate(var(--ttheta2)) scale(0); 70 } 71 } 72 `; 73 document.head.appendChild(style); 74 75 let lastTime = 0; 76 const throttleDelay = 30; 77 78 document.addEventListener("mousemove", (e) => { 79 const now = Date.now(); 80 if (now - lastTime < throttleDelay) return; 81 lastTime = now; 82 83 const star = document.createElement("div"); 84 star.className = "star"; 85 star.textContent = "✨"; 86 star.style.left = e.clientX + "px"; 87 star.style.top = e.clientY + "px"; 88 89 const tx = (Math.random() - 0.5) * 50; 90 const ty = (Math.random() - 0.5) * 50; 91 const ttheta1 = Math.random() * 360; 92 const ttheta2 = ttheta1 + (Math.random() - 0.5) * 540; 93 star.style.setProperty("--tx", tx + "px"); 94 star.style.setProperty("--ty", ty + "px"); 95 star.style.setProperty("--ttheta1", ttheta1 + "deg"); 96 star.style.setProperty("--ttheta2", ttheta2 + "deg"); 97 98 document.body.appendChild(star); 99 100 setTimeout(() => star.remove(), 800); 101 }); 102 } 103 }); 104 105 return ( 106 <div 107 id="main" 108 class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4 text-neutral-900 dark:text-neutral-200" 109 > 110 <MetaProvider> 111 <Show when={location.pathname !== "/"}> 112 <Meta name="robots" content="noindex, nofollow" /> 113 </Show> 114 </MetaProvider> 115 <header 116 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%)]" : ""}`} 117 style={{ 118 "background-image": 119 props.params.repo in headers ? 120 `linear-gradient(to left, transparent 10%, var(--header-bg) 85%), url(/headers/${headers[props.params.repo]})` 121 : undefined, 122 }} 123 > 124 <A 125 href="/" 126 style='font-feature-settings: "cv05"' 127 class="flex items-center gap-1 text-xl font-semibold" 128 > 129 <span class="iconify tabler--binary-tree-filled text-[#76c4e5]"></span> 130 <span>PDSls</span> 131 </A> 132 <div class="dark:bg-dark-300/60 relative flex items-center gap-1 rounded-lg bg-neutral-50/60"> 133 <Show when={location.pathname !== "/"}> 134 <SearchButton /> 135 </Show> 136 <Show when={agent()}> 137 <RecordEditor create={true} /> 138 </Show> 139 <AccountManager /> 140 <MenuProvider> 141 <DropdownMenu 142 icon="lucide--menu text-xl" 143 buttonClass="rounded-lg p-1" 144 menuClass="top-11 p-3 text-sm" 145 > 146 <NavMenu href="/jetstream" label="Jetstream" /> 147 <NavMenu href="/firehose" label="Firehose" /> 148 <NavMenu href="/labels" label="Labels" /> 149 <NavMenu href="/settings" label="Settings" /> 150 <MenuSeparator /> 151 <NavMenu 152 href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz" 153 label="Bluesky" 154 newTab 155 external 156 /> 157 <NavMenu 158 href="https://tangled.org/@pdsls.dev/pdsls/" 159 label="Source" 160 newTab 161 external 162 /> 163 <ThemeSelection /> 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-break-word">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 };