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 -mr-1 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 };