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-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%)]" : ""}`}
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-0.5 py-0.5 px-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-lg"
143 buttonClass="rounded-lg p-1.5"
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 };