an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm
at main 32 kB view raw
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}