an appview-less Bluesky client using Constellation and PDS Queries
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
1import * as TabsPrimitive from "@radix-ui/react-tabs";
2import { useAtom } from "jotai";
3import { useEffect, useLayoutEffect } from "react";
4
5import { isAtTopAtom, reusableTabRouteScrollAtom } from "~/utils/atoms";
6
7/**
8 * Please wrap your Route in a div, do not return a top-level fragment,
9 * it will break navigation scroll restoration
10 */
11export function ReusableTabRoute({
12 route,
13 tabs,
14}: {
15 route: string;
16 tabs: Record<string, React.ReactNode>;
17}) {
18 const [reusableTabState, setReusableTabState] = useAtom(
19 reusableTabRouteScrollAtom
20 );
21 const [isAtTop] = useAtom(isAtTopAtom);
22
23 const routeState = reusableTabState?.[route] ?? {
24 activeTab: Object.keys(tabs)[0],
25 scrollPositions: {},
26 };
27 const activeTab = routeState.activeTab;
28
29 const handleValueChange = (newTab: string) => {
30 setReusableTabState((prev) => {
31 const current = prev?.[route] ?? routeState;
32 return {
33 ...prev,
34 [route]: {
35 ...current,
36 scrollPositions: {
37 ...current.scrollPositions,
38 [current.activeTab]: window.scrollY,
39 },
40 activeTab: newTab,
41 },
42 };
43 });
44 };
45
46 // // todo, warning experimental, usually this doesnt work,
47 // // like at all, and i usually do this for each tab
48 // useLayoutEffect(() => {
49 // const savedScroll = routeState.scrollPositions[activeTab] ?? 0;
50 // window.scrollTo({ top: savedScroll });
51 // // eslint-disable-next-line react-hooks/exhaustive-deps
52 // }, [activeTab, route]);
53
54 useLayoutEffect(() => {
55 return () => {
56 setReusableTabState((prev) => {
57 const current = prev?.[route] ?? routeState;
58 return {
59 ...prev,
60 [route]: {
61 ...current,
62 scrollPositions: {
63 ...current.scrollPositions,
64 [current.activeTab]: window.scrollY,
65 },
66 },
67 };
68 });
69 };
70 // eslint-disable-next-line react-hooks/exhaustive-deps
71 }, []);
72
73 return (
74 <TabsPrimitive.Root
75 value={activeTab}
76 onValueChange={handleValueChange}
77 className={`w-full`}
78 >
79 <TabsPrimitive.List
80 className={`flex sticky top-[52px] bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-[9] border-0 sm:border-b ${!isAtTop && "shadow-sm"} sm:shadow-none sm:dark:bg-gray-950 sm:bg-white border-gray-200 dark:border-gray-700`}
81 >
82 {Object.entries(tabs).map(([key]) => (
83 <TabsPrimitive.Trigger key={key} value={key} className="m3tab">
84 {key}
85 </TabsPrimitive.Trigger>
86 ))}
87 </TabsPrimitive.List>
88
89 {Object.entries(tabs).map(([key, node]) => (
90 <TabsPrimitive.Content key={key} value={key} className="flex-1 min-h-[80dvh]">
91 {activeTab === key && node}
92 </TabsPrimitive.Content>
93 ))}
94 </TabsPrimitive.Root>
95 );
96}
97
98export function useReusableTabScrollRestore(route: string) {
99 const [reusableTabState] = useAtom(
100 reusableTabRouteScrollAtom
101 );
102
103 const routeState = reusableTabState?.[route];
104 const activeTab = routeState?.activeTab;
105
106 useEffect(() => {
107 const savedScroll = activeTab ? routeState?.scrollPositions[activeTab] ?? 0 : 0;
108 //window.scrollTo(0, savedScroll);
109 window.scrollTo({ top: savedScroll });
110 // eslint-disable-next-line react-hooks/exhaustive-deps
111 }, []);
112}
113
114
115/*
116
117 const [notifState] = useAtom(notificationsScrollAtom);
118 const activeTab = notifState.activeTab;
119 useEffect(() => {
120 const savedY = notifState.scrollPositions[activeTab] ?? 0;
121 window.scrollTo(0, savedY);
122 }, [activeTab, notifState.scrollPositions]);
123
124 */