an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
1import * as TabsPrimitive from "@radix-ui/react-tabs";
2import { useAtom } from "jotai";
3import { useEffect, useLayoutEffect, useRef, useState } 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 //const { sentinelRef, isStuck } = useSticky(52);
74 //bg-gray-100 dark:bg-gray-900
75
76 return (
77 <>
78 <TabsPrimitive.Root
79 value={activeTab}
80 onValueChange={handleValueChange}
81 className={`w-full`}
82 >
83 {/* <div ref={sentinelRef} className="h-[0.000000001px]" /> */}
84 <TabsPrimitive.List
85 className={`flex sticky top-[52px] bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] sm:dark:bg-gray-950 sm:bg-white z-[9] border-0 sm:border-b ${!isAtTop && "shadow-sm"} sm:shadow-none border-gray-200 dark:border-gray-700`}
86 >
87 {Object.entries(tabs).map(([key]) => (
88 <TabsPrimitive.Trigger key={key} value={key} className="m3tab">
89 {key}
90 </TabsPrimitive.Trigger>
91 ))}
92 </TabsPrimitive.List>
93
94 {Object.entries(tabs).map(([key, node]) => (
95 <TabsPrimitive.Content key={key} value={key} className="flex-1 min-h-[80dvh]">
96 {activeTab === key && node}
97 </TabsPrimitive.Content>
98 ))}
99 </TabsPrimitive.Root>
100 </>
101
102 );
103}
104
105export function useReusableTabScrollRestore(route: string) {
106 const [reusableTabState] = useAtom(
107 reusableTabRouteScrollAtom
108 );
109
110 const routeState = reusableTabState?.[route];
111 const activeTab = routeState?.activeTab;
112
113 useEffect(() => {
114 const savedScroll = activeTab ? routeState?.scrollPositions[activeTab] ?? 0 : 0;
115 //window.scrollTo(0, savedScroll);
116 window.scrollTo({ top: savedScroll });
117 // eslint-disable-next-line react-hooks/exhaustive-deps
118 }, []);
119}
120
121
122/*
123
124 const [notifState] = useAtom(notificationsScrollAtom);
125 const activeTab = notifState.activeTab;
126 useEffect(() => {
127 const savedY = notifState.scrollPositions[activeTab] ?? 0;
128 window.scrollTo(0, savedY);
129 }, [activeTab, notifState.scrollPositions]);
130
131 */
132
133
134
135export function useSticky(top: number = 0) {
136 const sentinelRef = useRef<HTMLDivElement | null>(null);
137 const [isStuck, setIsStuck] = useState(false);
138
139 useEffect(() => {
140 if (!sentinelRef.current) return;
141
142 const observer = new IntersectionObserver(
143 ([entry]) => setIsStuck(!entry.isIntersecting),
144 {
145 rootMargin: `-${top}px 0px 0px 0px`,
146 threshold: 0,
147 }
148 );
149
150 observer.observe(sentinelRef.current);
151 return () => observer.disconnect();
152 }, [top]);
153
154 return { sentinelRef, isStuck };
155}