Skip to content

Commit cdc58da

Browse files
Add a section sidebar to the account page
The account page stacked nine sections in one column. A sticky left nav on large screens (and a compact jump list on smaller ones) scrolls to each section and highlights the one in view. Co-authored-by: John Jeong <ComputelessComputer@users.noreply.github.com>
1 parent 2f683ac commit cdc58da

2 files changed

Lines changed: 159 additions & 73 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useEffect, useState } from "react";
2+
3+
import { cn } from "@anlg/utils";
4+
5+
export const ACCOUNT_SECTIONS = [
6+
{ id: "profile", label: "Profile" },
7+
{ id: "referrals", label: "Refer friends" },
8+
{ id: "plan", label: "Your plan" },
9+
{ id: "integrations", label: "Integrations" },
10+
{ id: "devices", label: "Synced devices" },
11+
{ id: "shares", label: "Shared notes" },
12+
{ id: "api-keys", label: "Cloud API keys" },
13+
{ id: "session", label: "Session controls" },
14+
{ id: "danger", label: "Danger area" },
15+
] as const;
16+
17+
export function useActiveAccountSection() {
18+
const [activeId, setActiveId] = useState<string>(ACCOUNT_SECTIONS[0].id);
19+
20+
useEffect(() => {
21+
const elements = ACCOUNT_SECTIONS.map((section) =>
22+
document.getElementById(section.id),
23+
).filter((element): element is HTMLElement => element !== null);
24+
25+
if (elements.length === 0) {
26+
return;
27+
}
28+
29+
const visibleIds = new Set<string>();
30+
const observer = new IntersectionObserver(
31+
(entries) => {
32+
for (const entry of entries) {
33+
if (entry.isIntersecting) {
34+
visibleIds.add(entry.target.id);
35+
} else {
36+
visibleIds.delete(entry.target.id);
37+
}
38+
}
39+
40+
const next = ACCOUNT_SECTIONS.find((section) =>
41+
visibleIds.has(section.id),
42+
);
43+
if (next) {
44+
setActiveId(next.id);
45+
}
46+
},
47+
{ rootMargin: "-20% 0px -65% 0px", threshold: [0, 0.25, 0.5] },
48+
);
49+
50+
for (const element of elements) {
51+
observer.observe(element);
52+
}
53+
54+
return () => observer.disconnect();
55+
}, []);
56+
57+
return activeId;
58+
}
59+
60+
export function AccountNav({ activeId }: { activeId: string }) {
61+
return (
62+
<nav aria-label="Account sections">
63+
<ul
64+
className={cn([
65+
"flex gap-1 overflow-x-auto px-5",
66+
"lg:flex-col lg:overflow-visible lg:px-0",
67+
])}
68+
>
69+
{ACCOUNT_SECTIONS.map((section) => {
70+
const isActive = section.id === activeId;
71+
72+
return (
73+
<li
74+
key={section.id}
75+
className={cn([
76+
"shrink-0",
77+
section.id === "danger" &&
78+
"lg:mt-2 lg:border-t lg:border-[#ede7dc] lg:pt-2",
79+
])}
80+
>
81+
<a
82+
href={`#${section.id}`}
83+
aria-current={isActive ? "true" : undefined}
84+
className={cn([
85+
"block rounded-full px-3 py-1.5 text-sm whitespace-nowrap transition-colors",
86+
isActive
87+
? "bg-[#fff0b3] font-medium text-[#181613]"
88+
: "text-[#756b5d] hover:text-[#181613]",
89+
!isActive && section.id === "danger" && "hover:text-red-700",
90+
])}
91+
>
92+
{section.label}
93+
</a>
94+
</li>
95+
);
96+
})}
97+
</ul>
98+
</nav>
99+
);
100+
}

apps/web/src/routes/_view/app/account.tsx

Lines changed: 59 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import { ApiKeysSection } from "./-account-api-keys";
1717
import { DangerAreaSection } from "./-account-danger";
1818
import { DevicesSection } from "./-account-devices";
1919
import { IntegrationsSection } from "./-account-integrations";
20+
import {
21+
AccountNav,
22+
ACCOUNT_SECTIONS,
23+
useActiveAccountSection,
24+
} from "./-account-nav";
2025
import { PlanSection } from "./-account-plan";
2126
import { ProfileInfoSection } from "./-account-profile-info";
2227
import { ReferralSection } from "./-account-referrals";
@@ -47,6 +52,7 @@ function Component() {
4752
const search = Route.useSearch();
4853
const { identify: identifyPosthog, track } = useAnalytics();
4954
const queryClient = useQueryClient();
55+
const activeSectionId = useActiveAccountSection();
5056

5157
useEffect(() => {
5258
if (!search.success && search.trial !== "started") {
@@ -103,7 +109,7 @@ function Component() {
103109

104110
return (
105111
<main className="min-h-screen bg-white text-[#181613]">
106-
<div className="mx-auto w-full max-w-[700px] px-5 pt-10 pb-16 md:px-8 md:pt-12 md:pb-24">
112+
<div className="mx-auto w-full max-w-[700px] px-5 pt-10 pb-16 md:px-8 md:pt-12 md:pb-24 lg:max-w-[980px]">
107113
<Link to="/" aria-label="Anarlog home" className="inline-flex">
108114
<AnarlogLogo className="h-8 w-auto" />
109115
</Link>
@@ -120,89 +126,69 @@ function Component() {
120126
</h1>
121127
</header>
122128

123-
<div className="mt-14 flex flex-col gap-14 md:mt-16">
124-
<section>
125-
<h2 className="font-hand text-3xl leading-none font-semibold text-[#756b5d]">
126-
Profile
127-
</h2>
128-
<div className="mt-6">
129+
<div className="isolate mt-10 grid items-start gap-8 md:mt-12 lg:mt-16 lg:grid-cols-[12rem_minmax(0,1fr)] lg:gap-x-12 lg:gap-y-0">
130+
<div className="sticky top-0 z-10 -mx-5 border-b border-[#ede7dc] bg-white py-3 md:-mx-8 lg:top-10 lg:mx-0 lg:border-0 lg:bg-transparent lg:py-0">
131+
<AccountNav activeId={activeSectionId} />
132+
</div>
133+
134+
<div className="flex min-w-0 flex-col gap-14">
135+
<AccountSection id="profile">
129136
<ProfileInfoSection email={user?.email} />
130-
</div>
131-
</section>
132-
133-
<section id="referrals" className="scroll-mt-8">
134-
<h2 className="font-hand text-3xl leading-none font-semibold text-[#756b5d]">
135-
Refer friends
136-
</h2>
137-
<div className="mt-6">
137+
</AccountSection>
138+
139+
<AccountSection id="referrals">
138140
<ReferralSection ineligible={search.referral === "ineligible"} />
139-
</div>
140-
</section>
141-
142-
<section>
143-
<h2 className="font-hand text-3xl leading-none font-semibold text-[#756b5d]">
144-
Your plan
145-
</h2>
146-
<div className="mt-6">
141+
</AccountSection>
142+
143+
<AccountSection id="plan">
147144
<PlanSection perk={search.perk} />
148-
</div>
149-
</section>
150-
151-
<section>
152-
<h2 className="font-hand text-3xl leading-none font-semibold text-[#756b5d]">
153-
Integrations
154-
</h2>
155-
<div className="mt-6">
145+
</AccountSection>
146+
147+
<AccountSection id="integrations">
156148
<IntegrationsSection />
157-
</div>
158-
</section>
159-
160-
<section>
161-
<h2 className="font-hand text-3xl leading-none font-semibold text-[#756b5d]">
162-
Synced devices
163-
</h2>
164-
<div className="mt-6">
149+
</AccountSection>
150+
151+
<AccountSection id="devices">
165152
<DevicesSection />
166-
</div>
167-
</section>
168-
169-
<section>
170-
<h2 className="font-hand text-3xl leading-none font-semibold text-[#756b5d]">
171-
Shared notes
172-
</h2>
173-
<div className="mt-6">
153+
</AccountSection>
154+
155+
<AccountSection id="shares">
174156
<SharedNotesSection />
175-
</div>
176-
</section>
177-
178-
<section>
179-
<h2 className="font-hand text-3xl leading-none font-semibold text-[#756b5d]">
180-
Cloud API keys
181-
</h2>
182-
<div className="mt-6">
157+
</AccountSection>
158+
159+
<AccountSection id="api-keys">
183160
<ApiKeysSection />
184-
</div>
185-
</section>
186-
187-
<section>
188-
<h2 className="font-hand text-3xl leading-none font-semibold text-[#756b5d]">
189-
Session controls
190-
</h2>
191-
<div className="mt-6">
161+
</AccountSection>
162+
163+
<AccountSection id="session">
192164
<AccountAccessSection />
193-
</div>
194-
</section>
195-
196-
<section>
197-
<h2 className="font-hand text-3xl leading-none font-semibold text-[#756b5d]">
198-
Danger area
199-
</h2>
200-
<div className="mt-6">
165+
</AccountSection>
166+
167+
<AccountSection id="danger">
201168
<DangerAreaSection />
202-
</div>
203-
</section>
169+
</AccountSection>
170+
</div>
204171
</div>
205172
</div>
206173
</main>
207174
);
208175
}
176+
177+
function AccountSection({
178+
id,
179+
children,
180+
}: {
181+
id: (typeof ACCOUNT_SECTIONS)[number]["id"];
182+
children: React.ReactNode;
183+
}) {
184+
const title = ACCOUNT_SECTIONS.find((section) => section.id === id)?.label;
185+
186+
return (
187+
<section id={id} className="scroll-mt-20 lg:scroll-mt-10">
188+
<h2 className="font-hand text-3xl leading-none font-semibold text-[#756b5d]">
189+
{title}
190+
</h2>
191+
<div className="mt-6">{children}</div>
192+
</section>
193+
);
194+
}

0 commit comments

Comments
 (0)