Skip to content

Commit eb7f209

Browse files
authored
Improve navbar visuals (sublinks#68)
* Improve navbar visuals * Only enable navbar hiding on certain pages * Implement review changes * Update api base url * Fix client side environment variables
1 parent 6439e59 commit eb7f209

File tree

19 files changed

+1972
-1335
lines changed

19 files changed

+1972
-1335
lines changed

src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const RootLayout = ({
4949
return (
5050
<ThemeProvider value={theme}>
5151
<html lang="en" className="h-full dark">
52-
<body className={cx(inter.className, 'flex flex-col h-full bg-secondary dark:bg-secondary-dark max-md:pb-48')}>
52+
<body className={cx(inter.className, 'flex flex-col min-h-full bg-secondary dark:bg-secondary-dark')}>
5353
<Header />
5454
<BottomNav />
5555

src/app/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import sublinksClient from '@/utils/client';
55
import { GetPostsResponse, GetSiteResponse } from 'sublinks-js-client';
66
import Feed from '@/components/front-page-feed';
77
import * as testData from '../../test-data.json';
8-
import * as testSiteData from '../../test-site-data.json';
8+
import * as testInstanceData from '../../test-instance-data.json';
99

1010
const page = async () => {
1111
// @todo: Allow test data when in non-docker dev env
@@ -15,7 +15,7 @@ const page = async () => {
1515

1616
const siteResponse = process.env.NEXT_PUBLIC_SUBLINKS_API_BASE_URL
1717
? await sublinksClient().getSite()
18-
: JSON.parse(JSON.stringify(testSiteData)) as unknown as GetSiteResponse;
18+
: JSON.parse(JSON.stringify(testInstanceData)) as unknown as GetSiteResponse;
1919

2020
return (
2121
<div>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use client';
2+
3+
import useScrollHeight from '@/hooks/use-scroll-height';
4+
import React, { useEffect, useState } from 'react';
5+
import cx from 'classnames';
6+
import { usePathname } from 'next/navigation';
7+
8+
const BottomNavDiv = ({ children }: { children: React.ReactNode }) => {
9+
const { wentUp } = useScrollHeight();
10+
const [navHidingEnabled, setNavHidingEnabled] = useState(true);
11+
const path = usePathname();
12+
13+
useEffect(() => {
14+
const regex = /\/c\//; // Paths for communities & posts (both share being after c)
15+
setNavHidingEnabled(path === '/' || regex.test(path));
16+
}, [path]);
17+
18+
return (
19+
<div className={cx('w-full h-48 flex items-center justify-around p-8 border-t bg-white z-10 dark:bg-black md:hidden backdrop-blur-lg border-gray-200 dark:border-gray-400 bg-opacity-60 dark:bg-opacity-60 fixed transition-all duration-300', (wentUp || !navHidingEnabled) ? 'bottom-0' : '-bottom-48')}>
20+
{ children }
21+
</div>
22+
);
23+
};
24+
25+
export default BottomNavDiv;

src/components/bottom-nav/index.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,24 @@ import {
66

77
import Icon, { ICON_SIZE } from '../icon';
88
import ProfileMenu from '../profile-menu';
9+
import BottomNavDiv from './bottom-nav-div';
910

1011
const BottomNav = () => (
11-
<div className="fixed bottom-0 w-full h-48 flex items-center justify-around py-8 px-8 md:px-16 border-t bg-white z-10 dark:bg-gray-500 dark:border-gray-900 md:hidden">
12-
<Link href="/communities"><Icon IconType={UserGroupIcon} size={ICON_SIZE.MEDIUM} title="Communities icon" isInteractable /></Link>
13-
<Link href="/p"><Icon IconType={DocumentPlusIcon} size={ICON_SIZE.MEDIUM} title="Create post icon" isInteractable /></Link>
14-
<button type="button" aria-label="Search"><Icon IconType={MagnifyingGlassIcon} size={ICON_SIZE.MEDIUM} title="Search icon" isInteractable /></button>
12+
<BottomNavDiv>
13+
<Link href="/communities">
14+
<Icon IconType={UserGroupIcon} size={ICON_SIZE.MEDIUM} title="Communities icon" isInteractable />
15+
</Link>
16+
17+
<Link href="/p">
18+
<Icon IconType={DocumentPlusIcon} size={ICON_SIZE.MEDIUM} title="Create post icon" isInteractable />
19+
</Link>
20+
21+
<button type="button" aria-label="Search">
22+
<Icon IconType={MagnifyingGlassIcon} size={ICON_SIZE.MEDIUM} title="Search icon" isInteractable />
23+
</button>
24+
1525
<ProfileMenu />
16-
</div>
26+
</BottomNavDiv>
1727
);
1828

1929
export default BottomNav;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use client';
2+
3+
import useScrollHeight from '@/hooks/use-scroll-height';
4+
import React from 'react';
5+
6+
const HeaderLayout = ({ children }: { children: React.ReactNode }) => {
7+
const { scrollHeight } = useScrollHeight();
8+
9+
return (
10+
<header className={`sticky top-0 z-40 hidden md:flex items-center justify-between ${scrollHeight > 100 ? 'h-48 min-h-48' : 'h-64 min-h-64'} px-24 border-b backdrop-blur-lg border-gray-200 dark:border-gray-400 transition-all duration-300 bg-white dark:bg-black bg-opacity-60 dark:bg-opacity-60`}>
11+
{children}
12+
</header>
13+
);
14+
};
15+
16+
export default HeaderLayout;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use client';
2+
3+
import useScrollHeight from '@/hooks/use-scroll-height';
4+
import React from 'react';
5+
import Image from 'next/image';
6+
7+
const HeaderLogoIcon = ({ icon }: { icon: string }) => {
8+
const { scrollHeight } = useScrollHeight();
9+
10+
return (
11+
<Image
12+
className={`${scrollHeight > 100 ? 'h-20 w-20 lg:h-32 lg:w-32' : 'h-24 w-24 lg:h-40 lg:w-40'} transition-all duration-300`}
13+
src={icon}
14+
alt="Site icon"
15+
width={40}
16+
height={40}
17+
priority
18+
/>
19+
);
20+
};
21+
22+
export default HeaderLogoIcon;

src/components/header/header-logo.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
import Link from 'next/link';
3+
import HeaderLogoIcon from './header-logo-icon';
4+
import { LinkText } from '../text';
5+
6+
const HeaderLogo = ({ name, icon }: { name: string; icon: string }) => (
7+
<Link href="/" className="flex items-center gap-8 lg:gap-16 text-sm lg:text-lg text-gray-900 dark:text-gray-100 hover:text-brand dark:hover:text-brand-dark transition-all duration-200">
8+
<HeaderLogoIcon icon={icon} />
9+
<LinkText>{name}</LinkText>
10+
</Link>
11+
);
12+
13+
export default HeaderLogo;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import cx from 'classnames';
3+
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
4+
import { InputField } from '../input';
5+
6+
const searchSharedClasses = 'group-focus-within:text-brand dark:group-focus-within:text-brand-dark dark:group-hover:text-brand-dark group-hover:text-brand';
7+
8+
const HeaderSearch = () => (
9+
<InputField
10+
type="text"
11+
name="search"
12+
id="search"
13+
label="Search"
14+
placeholder="Search"
15+
LeftIcon={MagnifyingGlassIcon}
16+
className="w-108 lg:w-200 xl:w-240 xl:hover:w-460 xl:focus-within:w-460 transition-all duration-300 group"
17+
iconClassName={searchSharedClasses}
18+
showBorderPlaceholder
19+
inputClassName={cx(searchSharedClasses, 'group-focus-within:placeholder-brand dark:group-focus-within:placeholder-brand-dark dark:group-hover:placeholder-brand-dark group-hover:placeholder-brand')}
20+
borderPlaceholderClassName={searchSharedClasses}
21+
/>
22+
);
23+
24+
export default HeaderSearch;

src/components/header/index.tsx

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,77 @@
11
import React from 'react';
2-
import Image from 'next/image';
3-
import Link from 'next/link';
4-
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
2+
import {
3+
BellIcon,
4+
ClipboardIcon,
5+
HeartIcon,
6+
ShieldExclamationIcon
7+
} from '@heroicons/react/24/outline';
58

6-
import { InputField } from '../input';
7-
import { LinkText } from '../text';
9+
import sublinksClient from '@/utils/client';
10+
import { GetSiteResponse } from 'sublinks-js-client';
11+
import Link from 'next/link';
812
import ProfileMenu from '../profile-menu';
13+
import HeaderLogo from './header-logo';
14+
import HeaderSearch from './header-search';
15+
import Icon, { ICON_SIZE } from '../icon';
16+
import HeaderLayout from './header-layout';
17+
import * as testData from '../../../test-instance-data.json';
18+
import { LinkText } from '../text';
919

10-
const Header = () => (
11-
<header className="hidden md:flex items-center justify-between py-8 px-8 md:px-16 bg-primary dark:bg-primary-dark">
12-
<div className="flex items-center">
13-
<Link href="/">
14-
<Image
15-
className="h-32 w-32"
16-
src="/logo.png"
17-
alt="Sublinks logo"
18-
width={32}
19-
height={32}
20-
priority
21-
/>
22-
</Link>
23-
<div className="flex gap-16 ml-24 items-center">
24-
<Link href="/communities">
25-
<LinkText>Create community</LinkText>
20+
const Header = async () => {
21+
const instance = process.env.NEXT_PUBLIC_SUBLINKS_API_BASE_URL ? await sublinksClient().getSite()
22+
: testData as unknown as GetSiteResponse;
23+
24+
return (
25+
<HeaderLayout>
26+
{/* Header Left Side */}
27+
<div className="flex gap-8 lg:gap-16 items-center text-sm lg:text-base">
28+
<HeaderLogo name={instance.site_view.site.name} icon={instance.site_view.site.icon || '/logo.png'} />
29+
30+
<p className="text-gray-200 dark:text-gray-400 hover:cursor-default">/</p>
31+
32+
<Link href="/c">
33+
<LinkText>Communities</LinkText>
2634
</Link>
35+
2736
<Link href="/p">
2837
<LinkText>Create post</LinkText>
2938
</Link>
39+
40+
<Link href="/create_community">
41+
<LinkText>Create community</LinkText>
42+
</Link>
43+
44+
<Link href="/donate">
45+
<Icon IconType={HeartIcon} size={ICON_SIZE.SMALL} title="Donate icon" isInteractable />
46+
</Link>
47+
</div>
48+
49+
{/* Header Right Side */}
50+
<div className="flex items-center gap-8 lg:gap-16 text-sm lg:text-base">
51+
<HeaderSearch />
52+
53+
{false && ( // Change to check if a user is logged in
54+
<Link href="/inbox">
55+
<Icon IconType={BellIcon} size={ICON_SIZE.SMALL} title="Inbox icon" isInteractable />
56+
</Link>
57+
)}
58+
59+
{false && ( // Change to check if a user is a mod or an admin
60+
<Link href="/reports">
61+
<Icon IconType={ShieldExclamationIcon} size={ICON_SIZE.SMALL} title="Reports icon" isInteractable />
62+
</Link>
63+
)}
64+
65+
{false && ( // Change to check if applications are enabled and the user is an admin
66+
<Link href="/registration_applications">
67+
<Icon IconType={ClipboardIcon} size={ICON_SIZE.SMALL} title="Registration applications icon" isInteractable />
68+
</Link>
69+
)}
70+
71+
<ProfileMenu />
3072
</div>
31-
</div>
32-
<div className="flex items-center gap-12">
33-
<InputField
34-
type="text"
35-
name="search"
36-
id="search"
37-
label="Search"
38-
placeholder="Search"
39-
LeftIcon={MagnifyingGlassIcon}
40-
className="w-240 lg:hover:w-500 lg:focus-within:w-500 transition-all"
41-
/>
42-
<ProfileMenu />
43-
</div>
44-
</header>
45-
);
73+
</HeaderLayout>
74+
);
75+
};
4676

4777
export default Header;

src/components/icon/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const Icon = ({
3838
}: IconProps) => (
3939
<div className={cx(wrapperSizeClassMap[size], {
4040
'text-gray-700 dark:text-white': !textClassName,
41-
'hover:text-brand dark:hover:text-brand-dark': isInteractable && !textClassName
41+
'hover:text-brand dark:hover:text-brand-dark transition-text duration-200': isInteractable && !textClassName
4242
}, className, textClassName)}
4343
>
4444
<IconType

0 commit comments

Comments
 (0)