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