forked from
pds.ls/pdsls
atmosphere explorer
1import { Handle } from "@atcute/lexicons";
2import { Meta, MetaProvider, Title } 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 { agent } from "./auth/state.js";
7import { RecordEditor } from "./components/create";
8import { DropdownMenu, MenuProvider, MenuSeparator, NavMenu } from "./components/dropdown.jsx";
9import { NavBar } from "./components/navbar.jsx";
10import { NotificationContainer } from "./components/notification.jsx";
11import { PermissionPromptContainer } from "./components/permission-prompt.jsx";
12import { Search, SearchButton } from "./components/search.jsx";
13import { themeEvent } from "./components/theme.jsx";
14import { resolveHandle } from "./utils/api.js";
15import { plcDirectory } from "./views/settings.jsx";
16
17export const canHover = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
18
19const headers: Record<string, string> = {
20 "did:plc:ia76kvnndjutgedggx2ibrem": "bunny.jpg",
21 "did:plc:oisofpd7lj26yvgiivf3lxsi": "puppy.jpg",
22 "did:plc:vwzwgnygau7ed7b7wt5ux7y2": "water.webp",
23 "did:plc:uu5axsmbm2or2dngy4gwchec": "city.webp",
24 "did:plc:aokggmp5jzj4nc5jifhiplqc": "bridge.jpg",
25 "did:plc:bnqkww7bjxaacajzvu5gswdf": "forest.jpg",
26 "did:plc:p2cp5gopk7mgjegy6wadk3ep": "aurora.jpg",
27 "did:plc:ucaezectmpny7l42baeyooxi": "almaty.webp",
28 "did:plc:7rfssi44thh6f4ywcl3u5nvt": "sonic.jpg",
29 "did:plc:6if5m2yo6kroprmmency3gt5": "montreal.webp",
30};
31
32const Layout = (props: RouteSectionProps<unknown>) => {
33 const location = useLocation();
34 const navigate = useNavigate();
35
36 if (location.search.includes("hrt=true")) localStorage.setItem("hrt", "true");
37 else if (location.search.includes("hrt=false")) localStorage.setItem("hrt", "false");
38 if (location.search.includes("sailor=true")) localStorage.setItem("sailor", "true");
39 else if (location.search.includes("sailor=false")) localStorage.setItem("sailor", "false");
40
41 createEffect(async () => {
42 if (props.params.repo && !props.params.repo.startsWith("did:")) {
43 const did = await resolveHandle(props.params.repo as Handle);
44 navigate(location.pathname.replace(props.params.repo, did), { replace: true });
45 }
46 });
47
48 onMount(() => {
49 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent);
50
51 const handleGoToRepo = (ev: KeyboardEvent) => {
52 if (document.querySelector("[data-modal]")) return;
53 if (ev.target instanceof HTMLInputElement || ev.target instanceof HTMLTextAreaElement) return;
54
55 if (ev.key === "g" && agent()?.sub) {
56 ev.preventDefault();
57 navigate(`/at://${agent()!.sub}`);
58 }
59 };
60
61 window.addEventListener("keydown", handleGoToRepo);
62 onCleanup(() => window.removeEventListener("keydown", handleGoToRepo));
63
64 if (localStorage.getItem("sailor") === "true") {
65 const style = document.createElement("style");
66 style.textContent = `
67 html, * {
68 cursor: url(/cursor.cur), pointer;
69 }
70
71 .star {
72 position: fixed;
73 pointer-events: none;
74 z-index: 9999;
75 font-size: 20px;
76 animation: sparkle 0.8s ease-out forwards;
77 }
78
79 @keyframes sparkle {
80 0% {
81 opacity: 1;
82 transform: translate(0, 0) rotate(var(--ttheta1)) scale(1);
83 }
84 100% {
85 opacity: 0;
86 transform: translate(var(--tx), var(--ty)) rotate(var(--ttheta2)) scale(0);
87 }
88 }
89 `;
90 document.head.appendChild(style);
91
92 let lastTime = 0;
93 const throttleDelay = 30;
94
95 document.addEventListener("mousemove", (e) => {
96 const now = Date.now();
97 if (now - lastTime < throttleDelay) return;
98 lastTime = now;
99
100 const star = document.createElement("div");
101 star.className = "star";
102 star.textContent = "✨";
103 star.style.left = e.clientX + "px";
104 star.style.top = e.clientY + "px";
105
106 const tx = (Math.random() - 0.5) * 50;
107 const ty = (Math.random() - 0.5) * 50;
108 const ttheta1 = Math.random() * 360;
109 const ttheta2 = ttheta1 + (Math.random() - 0.5) * 540;
110 star.style.setProperty("--tx", tx + "px");
111 star.style.setProperty("--ty", ty + "px");
112 star.style.setProperty("--ttheta1", ttheta1 + "deg");
113 star.style.setProperty("--ttheta2", ttheta2 + "deg");
114
115 document.body.appendChild(star);
116
117 setTimeout(() => star.remove(), 800);
118 });
119 }
120 });
121
122 return (
123 <MetaProvider>
124 <Title>PDSls</Title>
125 <Show when={location.pathname !== "/"}>
126 <Meta name="robots" content="noindex, nofollow" />
127 </Show>
128 <div id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-3">
129 <header
130 class={`dark:shadow-dark-700 mb-3 flex h-13 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] [--trans-blue:#5BCEFA90] [--trans-pink:#F5A9B890] [--trans-white:#FFFFFF90] dark:border-neutral-700 dark:bg-neutral-800 dark:[--header-bg:#262626] dark:[--trans-blue:#5BCEFAa0] dark:[--trans-pink:#F5A9B8a0] dark:[--trans-white:#FFFFFFa0] ${localStorage.getItem("hrt") === "true" ? "bg-[linear-gradient(to_left,transparent_10%,var(--header-bg)_85%),linear-gradient(to_bottom,var(--trans-blue)_0%,var(--trans-blue)_20%,var(--trans-pink)_20%,var(--trans-pink)_40%,var(--trans-white)_40%,var(--trans-white)_60%,var(--trans-pink)_60%,var(--trans-pink)_80%,var(--trans-blue)_80%,var(--trans-blue)_100%)]" : ""}`}
131 style={{
132 "background-image":
133 props.params.repo && props.params.repo in headers ?
134 `linear-gradient(to left, transparent 20%, var(--header-bg) 85%), url(/headers/${headers[props.params.repo]})`
135 : undefined,
136 }}
137 >
138 <A
139 href="/"
140 style='font-feature-settings: "cv05"'
141 class="relative flex items-center gap-1 text-xl font-semibold"
142 >
143 <span class="iconify tabler--binary-tree-filled text-[#76c4e5]"></span>
144 <span>PDSls</span>
145 <Show when={localStorage.getItem("hrt") === "true"}>
146 <img
147 src="/ribbon.webp"
148 alt=""
149 class="pointer-events-none absolute -top-3 -right-4 w-8 rotate-15"
150 />
151 </Show>
152 </A>
153 <div class="relative flex items-center gap-0.5 rounded-lg bg-neutral-50/60 p-1 dark:bg-neutral-800/60">
154 <div class="mr-1">
155 <SearchButton />
156 </div>
157 <Show when={agent()}>
158 <RecordEditor create={true} scope="create" />
159 </Show>
160 <AccountManager />
161 <MenuProvider>
162 <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-md p-1.5">
163 <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" />
164 <NavMenu href="/firehose" label="Firehose" icon="lucide--rss" />
165 <NavMenu href="/spacedust" label="Spacedust" icon="lucide--orbit" />
166 <MenuSeparator />
167 <NavMenu href="/labels" label="Labels" icon="lucide--tag" />
168 <NavMenu href="/car" label="Archive tools" icon="lucide--folder-archive" />
169 <MenuSeparator />
170 <NavMenu href="/settings" label="Settings" icon="lucide--settings" />
171 </DropdownMenu>
172 </MenuProvider>
173 </div>
174 </header>
175 <div class="flex w-full flex-col items-center gap-3 text-pretty">
176 <Search />
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 <PermissionPromptContainer />
196 <Show when={plcDirectory() !== "https://plc.directory"}>
197 <div class="dark:bg-dark-500 fixed right-0 bottom-0 left-0 z-10 flex items-center justify-center bg-neutral-100 px-3 py-1 text-xs">
198 <span>
199 PLC directory: <span class="font-medium">{plcDirectory()}</span>
200 </span>
201 </div>
202 </Show>
203 </div>
204 </MetaProvider>
205 );
206};
207
208export { Layout };