Skip to content

Commit d63c333

Browse files
committed
feat: sticky header
1 parent adf7321 commit d63c333

28 files changed

+867
-210
lines changed

package-lock.json

Lines changed: 45 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@radix-ui/react-avatar": "^1.1.2",
2222
"@radix-ui/react-dialog": "^1.1.4",
2323
"@radix-ui/react-dropdown-menu": "^2.1.4",
24+
"@radix-ui/react-hover-card": "^1.1.4",
2425
"@radix-ui/react-icons": "^1.3.2",
2526
"@radix-ui/react-label": "^2.1.1",
2627
"@radix-ui/react-popover": "^1.1.4",
@@ -53,7 +54,7 @@
5354
"i18next-browser-languagedetector": "^8.0.2",
5455
"i18next-pseudo": "^2.2.1",
5556
"lowlight": "^3.3.0",
56-
"lucide-react": "^0.344.0",
57+
"lucide-react": "^0.469.0",
5758
"next-themes": "^0.4.4",
5859
"react": "^18.3.1",
5960
"react-dom": "^18.3.1",
@@ -72,6 +73,7 @@
7273
"tailwind-scrollbar-hide": "^2.0.0",
7374
"tailwindcss-animate": "^1.0.7",
7475
"tailwindcss-safe-area": "^0.6.0",
76+
"tailwindcss-view-transitions": "^0.1.1",
7577
"zod": "^3.24.1",
7678
"zustand": "^4.5.1"
7779
},

src/components/FacetedText.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,14 @@ function ExternalLink({ href, children }: { href: string; children: React.ReactN
7474

7575
function Mention({ handle }: { handle: string }) {
7676
return (
77-
<Link to="/profile/$handle" params={{ handle }} className="text-blue-400">
77+
<Link
78+
to="/profile/$handle"
79+
params={{ handle }}
80+
className="text-blue-400"
81+
onClick={(event) => {
82+
event.stopPropagation();
83+
}}
84+
>
7885
<span className="text-purple-500 font-semibold">@{handle.replace('.bksy.social', '')}</span>
7986
</Link>
8087
);

src/components/FeedSelector.tsx

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import { Tab } from './ui/tab';
1010
import { useQueryClient } from '@tanstack/react-query';
1111
import { useOfflineStatus } from '@/hooks/useOfflineStatus';
1212
import { cn } from '@/lib/utils';
13+
import { StickyHeader } from './sticky-header';
14+
import AkariLogo from '@/../public/images/logo.svg';
15+
import { Image } from './ui/Image';
16+
import { HamburgerMenuIcon } from '@radix-ui/react-icons';
17+
import { HashIcon } from 'lucide-react';
18+
import { Button } from './ui/button';
1319

1420
export const FeedSelector = ({ columnNumber = 1 }: { columnNumber: number }) => {
1521
const isOffline = useOfflineStatus();
@@ -70,26 +76,52 @@ export const FeedSelector = ({ columnNumber = 1 }: { columnNumber: number }) =>
7076
{/* if there are less than 2 feeds, don't show the selector */}
7177
{feeds.length >= 2 && (
7278
<>
73-
<TabList label="feed selector" className={cn('sticky bg-background z-50', isOffline ? 'top-[46px]' : 'top-0')}>
74-
{data?.map((feed) => {
75-
return (
76-
<Tab
77-
id={feed.uri}
78-
name={feed.displayName}
79-
selectedTab={selectedFeed}
80-
key={feed.uri}
81-
onClick={() => {
82-
// if this tab is already selected we need to invalidate the associated query
83-
if (selectedFeed === feed.uri) {
84-
queryClient.invalidateQueries({
85-
queryKey: ['feed', { feed: selectedFeed }],
86-
});
87-
}
79+
<StickyHeader backButton={false} className="p-0 border-none flex flex-col">
80+
<div className="w-full flex items-center justify-between px-4 py-2">
81+
<HamburgerMenuIcon className="size-6" />
82+
<Button
83+
onClick={() => {
84+
// scroll to top and invalidate the query
85+
window.scrollTo({ top: 0 });
86+
}}
87+
variant="ghost"
88+
className="hover:bg-transparent active:scale-90"
89+
>
90+
<Image
91+
src={AkariLogo}
92+
alt="Akari"
93+
classNames={{
94+
image: 'size-10',
8895
}}
96+
clickable={false}
8997
/>
90-
);
91-
})}
92-
</TabList>
98+
</Button>
99+
<HashIcon className="size-6" />
100+
</div>
101+
<TabList
102+
label="feed selector"
103+
className={cn('sticky bg-background z-40', isOffline ? 'top-[46px]' : 'top-0')}
104+
>
105+
{data?.map((feed) => {
106+
return (
107+
<Tab
108+
id={feed.uri}
109+
name={feed.displayName}
110+
selectedTab={selectedFeed}
111+
key={feed.uri}
112+
onClick={async () => {
113+
// if this tab is already selected we need to invalidate the associated query
114+
if (selectedFeed === feed.uri) {
115+
await queryClient.invalidateQueries({
116+
queryKey: ['feed', { feed: selectedFeed }],
117+
});
118+
}
119+
}}
120+
/>
121+
);
122+
})}
123+
</TabList>
124+
</StickyHeader>
93125
{data?.map((feed) => (
94126
<Ariakit.TabPanel key={feed.uri} tabId={feed.uri}>
95127
<Timeline columnNumber={columnNumber} />

src/components/Navbar.tsx

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,45 @@ import { useBlueskyStore } from '../lib/bluesky/store';
66
import { BellIcon, HomeIcon, LogInIcon, MailIcon, SearchIcon, SettingsIcon, UserIcon } from 'lucide-react';
77
import { CreatePost } from './CreatePost';
88
import { cn } from '@/lib/utils';
9-
import { appName } from '@/config';
109
import { useQueryClient } from '@tanstack/react-query';
1110
import { useUnreadCount } from '@/lib/bluesky/hooks/useUnreadCount';
1211
import { useConversations } from '@/lib/bluesky/hooks/useConversations';
12+
import { useScollVisible } from '@/hooks/useScrollVisible';
13+
import { Avatar } from './ui/avatar';
14+
import { useProfile } from '@/lib/bluesky/hooks/useProfile';
1315

1416
const HomeLink = () => {
17+
const { t } = useTranslation('app');
1518
const location = useLocation();
1619
const queryClient = useQueryClient();
1720
return (
1821
<Link
1922
to="/"
20-
onClick={() => {
23+
onClick={async () => {
2124
// if we're on the homepage already we need to invalidate the associated query
2225
if (location.pathname === '/') {
2326
// @TODO: we should really be invalidating the query for the current feed not all feeds
24-
queryClient.invalidateQueries({
27+
await queryClient.invalidateQueries({
2528
queryKey: ['feed'],
2629
});
2730
}
2831
}}
32+
className="flex flex-row items-center gap-2 p-3 rounded-sm hover:no-underline hover:bg-gray-200 dark:hover:bg-gray-700"
2933
>
30-
<HomeIcon className="size-7 xl:hidden" />
31-
<h1 className="text-2xl font-bold hidden xl:block">{appName}</h1>
34+
<HomeIcon className="size-7 xl:size-6 active:scale-90" />
35+
<span className="hidden xl:block">{t('home')}</span>
3236
</Link>
3337
);
3438
};
3539

3640
const SearchLink = () => {
3741
const { t } = useTranslation('search');
3842
return (
39-
<Link to="/search">
40-
<SearchIcon className="size-7 xl:hidden" />
43+
<Link
44+
to="/search"
45+
className="flex flex-row items-center gap-2 p-3 rounded-sm hover:no-underline hover:bg-gray-200 dark:hover:bg-gray-700"
46+
>
47+
<SearchIcon className="size-7 xl:size-6 active:scale-90" />
4148
<span className="hidden xl:block">{t('search')}</span>
4249
</Link>
4350
);
@@ -48,9 +55,12 @@ const MessagesLink = () => {
4855
const { data: convos } = useConversations();
4956
const unreadCount = convos?.map((convo) => convo.unreadCount).reduce((a, b) => a + b, 0) ?? 0;
5057
return (
51-
<Link to="/messages">
52-
<div className="relative">
53-
<MailIcon className="size-7 xl:hidden" />
58+
<Link
59+
to="/messages"
60+
className="flex flex-row items-center gap-2 p-3 rounded-sm hover:no-underline hover:bg-gray-200 dark:hover:bg-gray-700"
61+
>
62+
<div className="relative active:scale-90">
63+
<MailIcon className="size-7 xl:size-6" />
5464
{unreadCount > 0 && (
5565
<span className="absolute top-0 right-0 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-100 bg-blue-500 rounded-full transform translate-x-1/2 -translate-y-1/2">
5666
{unreadCount}
@@ -66,9 +76,12 @@ const NotificationsLink = () => {
6676
const { t } = useTranslation('notifications');
6777
const { data: unreadCount } = useUnreadCount();
6878
return (
69-
<Link to="/notifications">
79+
<Link
80+
to="/notifications"
81+
className="flex flex-row items-center gap-2 p-3 rounded-sm hover:no-underline hover:bg-gray-200 dark:hover:bg-gray-700"
82+
>
7083
<div className="relative">
71-
<BellIcon className="size-7 xl:hidden" />
84+
<BellIcon className="size-7 xl:size-6 active:scale-90" />
7285
{unreadCount > 0 && (
7386
<span className="absolute top-0 right-0 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-100 bg-blue-500 rounded-full transform translate-x-1/2 -translate-y-1/2">
7487
{unreadCount}
@@ -92,8 +105,9 @@ const ProfileLink = () => {
92105
params={{
93106
handle: session?.handle,
94107
}}
108+
className="flex flex-row items-center gap-2 p-3 rounded-sm hover:no-underline hover:bg-gray-200 dark:hover:bg-gray-700"
95109
>
96-
<UserIcon className="size-7 xl:hidden" />
110+
<UserIcon className="size-7 xl:size-6 active:scale-90" />
97111
<span className="hidden xl:block">{t('profile')}</span>
98112
</Link>
99113
);
@@ -102,8 +116,11 @@ const ProfileLink = () => {
102116
const SettingsLink = () => {
103117
const { t } = useTranslation('app');
104118
return (
105-
<Link to="/settings">
106-
<SettingsIcon className="size-7 xl:hidden" />
119+
<Link
120+
to="/settings"
121+
className="flex flex-row items-center gap-2 p-3 rounded-sm hover:no-underline hover:bg-gray-200 dark:hover:bg-gray-700"
122+
>
123+
<SettingsIcon className="size-7 xl:size-6 active:scale-90" />
107124
<span className="hidden xl:block">{t('settings')}</span>
108125
</Link>
109126
);
@@ -113,7 +130,7 @@ const LoginButton = () => {
113130
const { t } = useTranslation('auth');
114131
return (
115132
<Link to="/login">
116-
<LogInIcon className="size-7 xl:hidden" />
133+
<LogInIcon className="size-7 xl:hidden active:scale-90" />
117134
<span className="hidden xl:block">{t('login.default')}</span>
118135
</Link>
119136
);
@@ -122,19 +139,26 @@ const LoginButton = () => {
122139
export const Navbar = () => {
123140
const { isAuthenticated } = useAuth();
124141
const location = useLocation();
142+
const isVisible = useScollVisible();
143+
const handle = useBlueskyStore((state) => state.session?.handle);
144+
const { data: profile } = useProfile({ handle });
125145

126146
return (
127147
<div
128148
className={cn(
129-
'bg-background text-foreground',
149+
'bg-background text-foreground sticky',
130150
// base
131-
'px-4 pt-1 z-50',
151+
'px-4 pt-1 z-40',
132152
// mobile
133153
'fixed bottom-0 left-0 right-0 pb-safe border-t border-gray-200 dark:border-gray-800',
134154
// tablet
135155
'md:top-0 md:right-auto md:border-r md:border-gray-200 md:dark:border-gray-800',
136156
// desktop
137157
'xl:bg-inherit xl:sticky xl:h-screen xl:border-none xl:top-0',
158+
// transition
159+
'transform transition-transform duration-200 ease-in-out',
160+
isVisible ? 'translate-y-0' : 'translate-y-full',
161+
'md:translate-y-0',
138162
)}
139163
>
140164
<div
@@ -147,6 +171,9 @@ export const Navbar = () => {
147171
'xl:space-y-0',
148172
)}
149173
>
174+
<div className="flex flex-row items-center gap-2 p-3">
175+
{handle && <Avatar handle={handle} avatar={profile?.avatar} hover={false} />}
176+
</div>
150177
<HomeLink />
151178
<SearchLink />
152179
{isAuthenticated && <MessagesLink />}

0 commit comments

Comments
 (0)