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