an appview-less Bluesky client using Constellation and PDS Queries
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
1/// <reference types="vite/client" />
2
3// dont forget to run this
4// npx @tanstack/router-cli generate
5import type { QueryClient } from "@tanstack/react-query";
6import {
7 createRootRouteWithContext,
8 // Link,
9 // Outlet,
10 Scripts,
11 useLocation,
12 useNavigate,
13} from "@tanstack/react-router";
14import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
15import { useAtom } from "jotai";
16import * as React from "react";
17import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive";
18
19import { Composer } from "~/components/Composer";
20import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
21import { Import } from "~/components/Import";
22import Login from "~/components/Login";
23import { NotFound } from "~/components/NotFound";
24import { FluentEmojiHighContrastGlowingStar } from "~/components/Star";
25import { LikeMutationQueueProvider } from "~/providers/LikeMutationQueueProvider";
26import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
27import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms";
28import { seo } from "~/utils/seo";
29
30export const Route = createRootRouteWithContext<{
31 queryClient: QueryClient;
32}>()({
33 head: () => ({
34 meta: [
35 {
36 charSet: "utf-8",
37 },
38 {
39 name: "viewport",
40 content: "width=device-width, initial-scale=1",
41 },
42 ...seo({
43 title: "Red Dwarf",
44 description: `Distributed Bluesky Client`,
45 }),
46 ],
47 links: [
48 {
49 rel: "apple-touch-icon",
50 sizes: "180x180",
51 href: "/apple-touch-icon.png",
52 },
53 {
54 rel: "icon",
55 type: "image/png",
56 sizes: "32x32",
57 href: "/redstar.png?whatwg",
58 },
59 {
60 rel: "icon",
61 type: "image/png",
62 sizes: "16x16",
63 href: "/redstar.png?whatwg",
64 },
65 { rel: "manifest", href: "/site.webmanifest", color: "#fffff" },
66 { rel: "icon", href: "/favicon.ico" },
67 ],
68 }),
69 errorComponent: import.meta.env.DEV
70 ? undefined
71 : (props) => (
72 <RootDocument>
73 <DefaultCatchBoundary {...props} />
74 </RootDocument>
75 ),
76 notFoundComponent: () => <NotFound />,
77 component: RootComponent,
78});
79
80function RootComponent() {
81 return (
82 <UnifiedAuthProvider>
83 <LikeMutationQueueProvider>
84 <RootDocument>
85 <KeepAliveProvider>
86 <KeepAliveOutlet />
87 </KeepAliveProvider>
88 </RootDocument>
89 </LikeMutationQueueProvider>
90 </UnifiedAuthProvider>
91 );
92}
93
94function RootDocument({ children }: { children: React.ReactNode }) {
95 useAtomCssVar(hueAtom, "--tw-gray-hue");
96 const location = useLocation();
97 const navigate = useNavigate();
98 const { agent } = useAuth();
99 const authed = !!agent?.did;
100 const isHome = location.pathname === "/";
101 const isNotifications = location.pathname.startsWith("/notifications");
102 const isProfile =
103 agent &&
104 (location.pathname === `/profile/${agent?.did}` ||
105 location.pathname === `/profile/${encodeURIComponent(agent?.did ?? "")}`);
106 const isSettings = location.pathname.startsWith("/settings");
107 const isSearch = location.pathname.startsWith("/search");
108 const isFeeds = location.pathname.startsWith("/feeds");
109
110 const locationEnum:
111 | "feeds"
112 | "search"
113 | "settings"
114 | "notifications"
115 | "profile"
116 | "home" = isFeeds
117 ? "feeds"
118 : isSearch
119 ? "search"
120 : isSettings
121 ? "settings"
122 : isNotifications
123 ? "notifications"
124 : isProfile
125 ? "profile"
126 : "home";
127
128 const [, setComposerPost] = useAtom(composerAtom);
129
130 return (
131 <>
132 <Composer />
133
134 <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
135 <nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start">
136 <div className="flex items-center gap-3 mb-4">
137 <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
138 <span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
139 Red Dwarf{" "}
140 {/* <span className="text-gray-500 dark:text-gray-400 text-sm">
141 lite
142 </span> */}
143 </span>
144 </div>
145 <MaterialNavItem
146 InactiveIcon={
147 <IconMaterialSymbolsHomeOutline className="w-6 h-6" />
148 }
149 ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
150 active={locationEnum === "home"}
151 onClickCallbback={() =>
152 navigate({
153 to: "/",
154 //params: { did: agent.assertDid },
155 })
156 }
157 text="Home"
158 />
159
160 <MaterialNavItem
161 InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
162 ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
163 active={locationEnum === "search"}
164 onClickCallbback={() =>
165 navigate({
166 to: "/search",
167 //params: { did: agent.assertDid },
168 })
169 }
170 text="Explore"
171 />
172 <MaterialNavItem
173 InactiveIcon={
174 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
175 }
176 ActiveIcon={
177 <IconMaterialSymbolsNotifications className="w-6 h-6" />
178 }
179 active={locationEnum === "notifications"}
180 onClickCallbback={() =>
181 navigate({
182 to: "/notifications",
183 //params: { did: agent.assertDid },
184 })
185 }
186 text="Notifications"
187 />
188 <MaterialNavItem
189 InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
190 ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
191 active={locationEnum === "feeds"}
192 onClickCallbback={() =>
193 navigate({
194 to: "/feeds",
195 //params: { did: agent.assertDid },
196 })
197 }
198 text="Feeds"
199 />
200 <MaterialNavItem
201 InactiveIcon={
202 <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
203 }
204 ActiveIcon={
205 <IconMaterialSymbolsAccountCircle className="w-6 h-6" />
206 }
207 active={locationEnum === "profile"}
208 onClickCallbback={() => {
209 if (authed && agent && agent.assertDid) {
210 //window.location.href = `/profile/${agent.assertDid}`;
211 navigate({
212 to: "/profile/$did",
213 params: { did: agent.assertDid },
214 });
215 }
216 }}
217 text="Profile"
218 />
219 <MaterialNavItem
220 InactiveIcon={
221 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
222 }
223 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
224 active={locationEnum === "settings"}
225 onClickCallbback={() =>
226 navigate({
227 to: "/settings",
228 //params: { did: agent.assertDid },
229 })
230 }
231 text="Settings"
232 />
233 <div className="flex flex-row items-center justify-center mt-3">
234 <MaterialPillButton
235 InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
236 ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
237 //active={true}
238 onClickCallbback={() => setComposerPost({ kind: 'root' })}
239 text="Post"
240 />
241 </div>
242 {/* <Link
243 to="/"
244 className={
245 `py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` +
246 (isHome ? "font-bold" : "")
247 }
248 >
249 {!isHome ? (
250 <IconMaterialSymbolsHomeOutline width={28} height={28} />
251 ) : (
252 <IconMaterialSymbolsHome width={28} height={28} />
253 )}
254 <span>Home</span>
255 </Link>
256 <Link
257 to="/notifications"
258 className={
259 `py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` +
260 (isNotifications ? "font-bold" : "")
261 }
262 >
263 {!isNotifications ? (
264 <IconMaterialSymbolsNotificationsOutline width={28} height={28} />
265 ) : (
266 <IconMaterialSymbolsNotifications width={28} height={28} />
267 )}
268 <span>Notifications</span>
269 </Link>
270 <Link
271 to="/feeds"
272 className={`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ${
273 location.pathname.startsWith("/feeds") ? "font-bold" : ""
274 }`}
275 >
276 {location.pathname.startsWith("/feeds") ? (
277 <IconMaterialSymbolsTag width={28} height={28} />
278 ) : (
279 <IconMaterialSymbolsTag width={28} height={28} />
280 )}
281 <span>Feeds</span>
282 </Link>
283
284 <Link
285 to="/search"
286 className={`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ${
287 location.pathname.startsWith("/search") ? "font-bold" : ""
288 }`}
289 >
290 {location.pathname.startsWith("/search") ? (
291 <IconMaterialSymbolsSearch width={28} height={28} />
292 ) : (
293 <IconMaterialSymbolsSearch width={28} height={28} />
294 )}
295 <span>Search</span>
296 </Link>
297 <button
298 className={`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 w-full text-left ${
299 isProfile ? "bg-gray-100 dark:bg-gray-900 font-bold" : ""
300 }`}
301 onClick={() => {
302 if (authed && agent && agent.assertDid) {
303 //window.location.href = `/profile/${agent.assertDid}`;
304 navigate({
305 to: "/profile/$did",
306 params: { did: agent.assertDid },
307 });
308 }
309 }}
310 type="button"
311 >
312 {!isProfile ? (
313 <IconMaterialSymbolsAccountCircleOutline width={28} height={28} />
314 ) : (
315 <IconMaterialSymbolsAccountCircle width={28} height={28} />
316 )}
317 <span>Profile</span>
318 </button>
319 <Link
320 to="/settings"
321 className={`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ${
322 location.pathname.startsWith("/settings") ? "font-bold" : ""
323 }`}
324 >
325 {!location.pathname.startsWith("/settings") ? (
326 <IconMaterialSymbolsSettingsOutline width={28} height={28} />
327 ) : (
328 <IconMaterialSymbolsSettings width={28} height={28} />
329 )}
330 <span>Settings</span>
331 </Link> */}
332 {/* <button
333 className="mt-4 w-full flex items-center justify-center gap-3 py-3 px-0 mb-3 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 text-xl font-bold rounded-full transition-colors shadow"
334 onClick={() => setPostOpen(true)}
335 type="button"
336 >
337 <IconMdiPencilOutline
338 width={24}
339 height={24}
340 className="text-gray-600 dark:text-gray-400"
341 />
342 <span>Post</span>
343 </button> */}
344 <div className="flex-1"></div>
345 <a
346 href="https://tangled.sh/@whey.party/red-dwarf"
347 target="_blank"
348 rel="noopener noreferrer"
349 className="mt-1 text-xs text-gray-400 dark:text-gray-500 text-center hover:underline"
350 >
351 git repo
352 </a>
353 <a
354 href="https://whey.party/"
355 target="_blank"
356 rel="noopener noreferrer"
357 className="mt-1 text-xs text-gray-400 dark:text-gray-500 text-center hover:underline"
358 >
359 made by @whey.party
360 </a>
361 <div className="mt-2 text-xs text-gray-400 dark:text-gray-500 text-center">
362 powered by{" "}
363 <a
364 href="https://microcosm.blue"
365 target="_blank"
366 rel="noopener noreferrer"
367 className="underline hover:text-blue-500"
368 >
369 microcosm.blue
370 </a>
371 </div>
372 </nav>
373
374 <nav className="hidden sm:flex items-center lg:hidden h-screen flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start">
375 <div className="flex items-center gap-3 mb-4">
376 <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
377 </div>
378 <MaterialNavItem
379 small
380 InactiveIcon={
381 <IconMaterialSymbolsHomeOutline className="w-6 h-6" />
382 }
383 ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
384 active={locationEnum === "home"}
385 onClickCallbback={() =>
386 navigate({
387 to: "/",
388 //params: { did: agent.assertDid },
389 })
390 }
391 text="Home"
392 />
393
394 <MaterialNavItem
395 small
396 InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
397 ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
398 active={locationEnum === "search"}
399 onClickCallbback={() =>
400 navigate({
401 to: "/search",
402 //params: { did: agent.assertDid },
403 })
404 }
405 text="Explore"
406 />
407 <MaterialNavItem
408 small
409 InactiveIcon={
410 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
411 }
412 ActiveIcon={
413 <IconMaterialSymbolsNotifications className="w-6 h-6" />
414 }
415 active={locationEnum === "notifications"}
416 onClickCallbback={() =>
417 navigate({
418 to: "/notifications",
419 //params: { did: agent.assertDid },
420 })
421 }
422 text="Notifications"
423 />
424 <MaterialNavItem
425 small
426 InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
427 ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
428 active={locationEnum === "feeds"}
429 onClickCallbback={() =>
430 navigate({
431 to: "/feeds",
432 //params: { did: agent.assertDid },
433 })
434 }
435 text="Feeds"
436 />
437 <MaterialNavItem
438 small
439 InactiveIcon={
440 <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
441 }
442 ActiveIcon={
443 <IconMaterialSymbolsAccountCircle className="w-6 h-6" />
444 }
445 active={locationEnum === "profile"}
446 onClickCallbback={() => {
447 if (authed && agent && agent.assertDid) {
448 //window.location.href = `/profile/${agent.assertDid}`;
449 navigate({
450 to: "/profile/$did",
451 params: { did: agent.assertDid },
452 });
453 }
454 }}
455 text="Profile"
456 />
457 <MaterialNavItem
458 small
459 InactiveIcon={
460 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
461 }
462 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
463 active={locationEnum === "settings"}
464 onClickCallbback={() =>
465 navigate({
466 to: "/settings",
467 //params: { did: agent.assertDid },
468 })
469 }
470 text="Settings"
471 />
472 <div className="flex flex-row items-center justify-center mt-3">
473 <MaterialPillButton
474 small
475 InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
476 ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
477 //active={true}
478 onClickCallbback={() => setComposerPost({ kind: 'root' })}
479 text="Post"
480 />
481 </div>
482 </nav>
483
484 {agent?.did && (
485 <button
486 className="lg:hidden fixed bottom-22 right-4 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 rounded-2xl w-14 h-14 flex items-center justify-center transition-all"
487 style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
488 onClick={() => setComposerPost({ kind: 'root' })}
489 type="button"
490 aria-label="Create Post"
491 >
492 <IconMdiPencilOutline
493 width={24}
494 height={24}
495 className="text-gray-600 dark:text-gray-400"
496 />
497 </button>
498 )}
499
500 <main className="w-full max-w-[600px] sm:border-x border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 pb-16 lg:pb-0 overflow-x-clip">
501 {children}
502 </main>
503
504 <aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col">
505 <div className="px-4 pt-4"><Import /></div>
506 <Login />
507
508 <div className="flex-1"></div>
509 <p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
510 Red Dwarf is a Bluesky client that does not rely on any Bluesky API App Servers. Instead, it uses Microcosm to fetch records directly from each users' PDS (via Slingshot) and connect them using backlinks (via Constellation)
511 </p>
512 </aside>
513 </div>
514
515 {agent?.did ? (
516 <nav className="sm:hidden fixed bottom-0 left-0 right-0 bg-gray-50 dark:bg-gray-900 border-0 shadow border-gray-200 dark:border-gray-700 z-40">
517 <div className="flex justify-around items-center p-2">
518 <MaterialNavItem
519 small
520 InactiveIcon={
521 <IconMaterialSymbolsHomeOutline className="w-6 h-6" />
522 }
523 ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
524 active={locationEnum === "home"}
525 onClickCallbback={() =>
526 navigate({
527 to: "/",
528 //params: { did: agent.assertDid },
529 })
530 }
531 text="Home"
532 />
533 {/* <Link
534 to="/"
535 className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
536 isHome
537 ? "text-gray-900 dark:text-gray-100"
538 : "text-gray-600 dark:text-gray-400"
539 }`}
540 >
541 {!isHome ? (
542 <IconMaterialSymbolsHomeOutline width={24} height={24} />
543 ) : (
544 <IconMaterialSymbolsHome width={24} height={24} />
545 )}
546 <span className="text-xs mt-1">Home</span>
547 </Link> */}
548 <MaterialNavItem
549 small
550 InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
551 ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
552 active={locationEnum === "search"}
553 onClickCallbback={() =>
554 navigate({
555 to: "/search",
556 //params: { did: agent.assertDid },
557 })
558 }
559 text="Explore"
560 />
561 {/* <Link
562 to="/search"
563 className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
564 location.pathname.startsWith("/search")
565 ? "text-gray-900 dark:text-gray-100"
566 : "text-gray-600 dark:text-gray-400"
567 }`}
568 >
569 {!location.pathname.startsWith("/search") ? (
570 <IconMaterialSymbolsSearch width={24} height={24} />
571 ) : (
572 <IconMaterialSymbolsSearch width={24} height={24} />
573 )}
574 <span className="text-xs mt-1">Search</span>
575 </Link> */}
576 <MaterialNavItem
577 small
578 InactiveIcon={
579 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
580 }
581 ActiveIcon={
582 <IconMaterialSymbolsNotifications className="w-6 h-6" />
583 }
584 active={locationEnum === "notifications"}
585 onClickCallbback={() =>
586 navigate({
587 to: "/notifications",
588 //params: { did: agent.assertDid },
589 })
590 }
591 text="Notifications"
592 />
593 {/* <Link
594 to="/notifications"
595 className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
596 isNotifications
597 ? "text-gray-900 dark:text-gray-100"
598 : "text-gray-600 dark:text-gray-400"
599 }`}
600 >
601 {!isNotifications ? (
602 <IconMaterialSymbolsNotificationsOutline
603 width={24}
604 height={24}
605 />
606 ) : (
607 <IconMaterialSymbolsNotifications width={24} height={24} />
608 )}
609 <span className="text-xs mt-1">Notifications</span>
610 </Link> */}
611 <MaterialNavItem
612 small
613 InactiveIcon={
614 <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
615 }
616 ActiveIcon={
617 <IconMaterialSymbolsAccountCircle className="w-6 h-6" />
618 }
619 active={locationEnum === "profile"}
620 onClickCallbback={() => {
621 if (authed && agent && agent.assertDid) {
622 //window.location.href = `/profile/${agent.assertDid}`;
623 navigate({
624 to: "/profile/$did",
625 params: { did: agent.assertDid },
626 });
627 }
628 }}
629 text="Profile"
630 />
631 {/* <button
632 className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
633 isProfile
634 ? "text-gray-900 dark:text-gray-100"
635 : "text-gray-600 dark:text-gray-400"
636 }`}
637 onClick={() => {
638 if (authed && agent && agent.assertDid) {
639 //window.location.href = `/profile/${agent.assertDid}`;
640 navigate({
641 to: "/profile/$did",
642 params: { did: agent.assertDid },
643 });
644 }
645 }}
646 type="button"
647 >
648 <IconMaterialSymbolsAccountCircleOutline width={24} height={24} />
649 <span className="text-xs mt-1">Profile</span>
650 </button> */}
651 <MaterialNavItem
652 small
653 InactiveIcon={
654 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
655 }
656 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
657 active={locationEnum === "settings"}
658 onClickCallbback={() =>
659 navigate({
660 to: "/settings",
661 //params: { did: agent.assertDid },
662 })
663 }
664 text="Settings"
665 />
666 {/* <Link
667 to="/settings"
668 className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
669 location.pathname.startsWith("/settings")
670 ? "text-gray-900 dark:text-gray-100"
671 : "text-gray-600 dark:text-gray-400"
672 }`}
673 >
674 {!location.pathname.startsWith("/settings") ? (
675 <IconMaterialSymbolsSettingsOutline width={24} height={24} />
676 ) : (
677 <IconMaterialSymbolsSettings width={24} height={24} />
678 )}
679 <span className="text-xs mt-1">Settings</span>
680 </Link> */}
681 </div>
682 </nav>
683 ) : (
684 <div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10">
685 <div className="flex items-center gap-2">
686 <FluentEmojiHighContrastGlowingStar className="h-6 w-6" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} />
687 <span className="font-bold text-lg text-gray-900 dark:text-gray-100">
688 Red Dwarf{" "}
689 {/* <span className="text-gray-500 dark:text-gray-400 text-sm">
690 lite
691 </span> */}
692 </span>
693 </div>
694 <div className="flex items-center gap-2">
695 <Login compact={true} popup={true} />
696 </div>
697 </div>
698 )}
699
700 <TanStackRouterDevtools position="bottom-left" />
701 <Scripts />
702 </>
703 );
704}
705
706function MaterialNavItem({
707 InactiveIcon,
708 ActiveIcon,
709 text,
710 active,
711 onClickCallbback,
712 small,
713}: {
714 InactiveIcon: React.ReactElement;
715 ActiveIcon: React.ReactElement;
716 text: string;
717 active: boolean;
718 onClickCallbback: () => void;
719 small?: boolean | string;
720}) {
721 if (small)
722 return (
723 <button
724 className={`flex flex-col items-center rounded-lg transition-colors ${small} gap-1 ${
725 active
726 ? "text-gray-900 dark:text-gray-100"
727 : "text-gray-600 dark:text-gray-400"
728 }`}
729 onClick={() => {
730 onClickCallbback();
731 }}
732 >
733 <div
734 className={`px-4 py-1 rounded-full flex items-center justify-center ${active ? " bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 hover:dark:bg-gray-700" : "hover:bg-gray-50 hover:dark:bg-gray-900"}`}
735 >
736 {active ? ActiveIcon : InactiveIcon}
737 </div>
738 <span
739 className={`text-[12.8px] text-roboto ${active ? "font-medium" : ""}`}
740 >
741 {text}
742 </span>
743 </button>
744 );
745
746 return (
747 <button
748 className={`flex flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 w-full items-center rounded-full transition-colors flex-1 gap-1 ${
749 active
750 ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-800 bg-gray-200 hover:dark:bg-gray-700"
751 : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900"
752 }`}
753 onClick={() => {
754 onClickCallbback();
755 }}
756 >
757 <div className={`mr-4 ${active ? " " : " "}`}>
758 {active ? ActiveIcon : InactiveIcon}
759 </div>
760 <span
761 className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
762 >
763 {text}
764 </span>
765 </button>
766 );
767}
768
769function MaterialPillButton({
770 InactiveIcon,
771 ActiveIcon,
772 text,
773 //active,
774 onClickCallbback,
775 small,
776}: {
777 InactiveIcon: React.ReactElement;
778 ActiveIcon: React.ReactElement;
779 text: string;
780 //active: boolean;
781 onClickCallbback: () => void;
782 small?: boolean | string;
783}) {
784 const active = false;
785 return (
786 <button
787 className={`flex border border-gray-400 dark:border-gray-400 flex-row h-12 min-h-12 max-h-12 ${small ? "p-3 w-12" : "px-4 py-0.5"} items-center rounded-full transition-colors gap-1 ${
788 active
789 ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
790 : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
791 }`}
792 onClick={() => {
793 onClickCallbback();
794 }}
795 >
796 <div className={`${!small && "mr-2"} ${active ? " " : " "}`}>
797 {active ? ActiveIcon : InactiveIcon}
798 </div>
799 {!small && (
800 <span
801 className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
802 >
803 {text}
804 </span>
805 )}
806 </button>
807 );
808}