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