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}; 31 32const Layout = (props: RouteSectionProps<unknown>) => { 33 const location = useLocation(); 34 const navigate = useNavigate(); 35 let timeout: number; 36 37 if (location.search.includes("hrt=true")) localStorage.setItem("hrt", "true"); 38 else if (location.search.includes("hrt=false")) localStorage.setItem("hrt", "false"); 39 40 createEffect(async () => { 41 if (props.params.repo && !props.params.repo.startsWith("did:")) { 42 const did = await resolveHandle(props.params.repo as Handle); 43 navigate(location.pathname.replace(props.params.repo, did)); 44 } 45 }); 46 47 createEffect(() => { 48 if (notif().show) { 49 clearTimeout(timeout); 50 timeout = setTimeout(() => setNotif({ show: false }), 3000); 51 } 52 }); 53 54 onMount(() => { 55 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent); 56 }); 57 58 return ( 59 <div 60 id="main" 61 class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4 text-neutral-900 dark:text-neutral-200" 62 > 63 <MetaProvider> 64 <Show when={location.pathname !== "/"}> 65 <Meta name="robots" content="noindex, nofollow" /> 66 </Show> 67 </MetaProvider> 68 <header 69 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-2 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%)]" : ""}`} 70 style={{ 71 "background-image": 72 props.params.repo in headers ? 73 `linear-gradient(to left, transparent 10%, var(--header-bg) 85%), url(/headers/${headers[props.params.repo]})` 74 : undefined, 75 }} 76 > 77 <A 78 href="/" 79 style='font-feature-settings: "cv05"' 80 class="flex items-center gap-1 text-xl font-semibold" 81 > 82 <span class="iconify tabler--binary-tree-filled text-[#76c4e5]"></span> 83 <span>PDSls</span> 84 </A> 85 <div class="dark:bg-dark-300/60 relative flex items-center gap-1 rounded-lg bg-neutral-50/60"> 86 <Show when={location.pathname !== "/"}> 87 <SearchButton /> 88 </Show> 89 <Show when={agent()}> 90 <RecordEditor create={true} /> 91 </Show> 92 <AccountManager /> 93 <MenuProvider> 94 <DropdownMenu 95 icon="lucide--menu text-xl" 96 buttonClass="rounded-lg p-1" 97 menuClass="top-10 p-3" 98 > 99 <NavMenu href="/jetstream" label="Jetstream" /> 100 <NavMenu href="/firehose" label="Firehose" /> 101 <NavMenu href="/settings" label="Settings" /> 102 <ThemeSelection /> 103 </DropdownMenu> 104 </MenuProvider> 105 </div> 106 </header> 107 <div class="flex w-full flex-col items-center gap-3 text-pretty"> 108 <Show when={showSearch() || location.pathname === "/"}> 109 <Search /> 110 </Show> 111 <Show when={props.params.pds}> 112 <NavBar params={props.params} /> 113 </Show> 114 <Show keyed when={location.pathname}> 115 <ErrorBoundary 116 fallback={(err) => <div class="mt-3 wrap-break-word">Error: {err.message}</div>} 117 > 118 <Suspense 119 fallback={ 120 <span class="iconify lucide--loader-circle mt-3 animate-spin text-xl"></span> 121 } 122 > 123 {props.children} 124 </Suspense> 125 </ErrorBoundary> 126 </Show> 127 </div> 128 <Show when={notif().show}> 129 <button 130 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" 131 onClick={() => setNotif({ show: false })} 132 > 133 <span class={`iconify ${notif().icon} mr-1`}></span> 134 {notif().text} 135 </button> 136 </Show> 137 </div> 138 ); 139}; 140 141export { Layout };