Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

456 fe content panel improvements in navigation new branch #465

Open
wants to merge 6 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions api/utils/HOC/useClickOutside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect, RefObject } from 'react';

type Event = MouseEvent | TouchEvent;

const useClickOutside = <T extends HTMLElement = HTMLElement>(
ref: RefObject<T>,
handler: (event: Event) => void
) => {
useEffect(() => {
const listener = (event: Event) => {
const el = ref?.current;
if (!el || el.contains((event?.target as Node) || null)) {
return;
}

handler(event);
};

document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);

return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
};

export default useClickOutside;
14 changes: 14 additions & 0 deletions components/reusable-components/_Types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,17 @@ export interface BlogDetailsData {
publishDate: string;
tags: string[];
}

export enum UserStatus {
Active = 'active',
Banned = 'banned',
Deleted = 'deleted',
}

export interface User {
email: string;
first_name: string;
last_name: string;
status: UserStatus;
image: File | null;
}
13 changes: 10 additions & 3 deletions components/reusable-components/button/LogoutButton.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
'use client';

import React from 'react';
import React, { ReactElement } from 'react';
import useLogout from '../../../apis/mutations/login/useLogout';

const LogoutButton = ({ redirectUrl }: { redirectUrl: string }) => {
interface LogoutButtonProps {
redirectUrl: string;
className?: string;
icon?: ReactElement;
}

const LogoutButton: React.FC<LogoutButtonProps> = ({ redirectUrl, className, icon }) => {
const logout = useLogout(redirectUrl);

return (
<button type="button" onClick={() => logout.mutate()}>
<button type="button" onClick={() => logout.mutate()} className={className}>
{icon}
Logout
</button>
);
Expand Down
142 changes: 114 additions & 28 deletions components/reusable-components/navigation/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,137 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */

'use client';

import React from 'react';
import React, { useState, useRef } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import 'bootstrap-icons/font/bootstrap-icons.css';

import styles from './navigation.module.scss';
import { useTheme } from '../../../app/context/themeContext';
import { usePathname } from 'next/navigation';
import { useSession } from 'next-auth/react';
import { FaUser, FaSignOutAlt, FaChevronDown, FaChevronUp, FaEnvelope } from 'react-icons/fa';
import { Session } from 'next-auth';
import sunImage from '../../../public/theme/sun.png';
import moonImage from '../../../public/theme/moon.png';
import LogoNavigation from '../../../public/logo/logo-black.svg';
import styles from './navigation.module.scss';
import { useTheme } from '../../../app/context/themeContext';
import { User, UserStatus } from '../_Types';
import useClickOutside from '../../../api/utils/HOC/useClickOutside';
import LogoutButton from '../button/LogoutButton';

function extractUserFromSession(session: Session | null): User {
return {
email: typeof session?.user?.email === 'string' ? session.user.email : '',
mitkapanarin marked this conversation as resolved.
Show resolved Hide resolved
firstName: typeof session?.user?.name === 'string' ? session.user.name.split(' ')[0] : 'Name',
lastName: typeof session?.user?.name === 'string' ? session.user.name.split(' ')[1] || '' : '',
status: UserStatus.Active,
image: null,
};
}

const Navigation = () => {
const { data: session } = useSession();
const user: User = extractUserFromSession(session);
const { theme, toggleTheme } = useTheme();
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const menuRef = useRef<HTMLDivElement | null>(null);
const pathname = usePathname();
const isContentPanel = pathname.startsWith('/content-panel');
const isSun = theme === 'light';

useClickOutside(menuRef, () => {
if (isMenuOpen) setIsMenuOpen(false);
});

const renderAvatarMenu = () => (
<div className={styles.avatarMenu}>
<Link href="/content-panel/dashboard" className={styles.menuItem}>
<FaUser /> {`${user.firstName} ${user.lastName}`}
</Link>
<div className={styles.userEmail}>
<FaEnvelope /> {user.email}
</div>
<LogoutButton redirectUrl="/" className={styles.menuItem} icon={<FaSignOutAlt />} />
</div>
);

const handleKeyDown = (callback: () => void) => (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
callback();
}
};

const getImageSrc = (image: string | File | null): string => {
if (typeof image === 'string') return image;
if (image instanceof File) return URL.createObjectURL(image);
return '/user.png';
};

const contentPanelRoutes = [
{ path: '/content-panel/blogs', name: 'Blogs' },
{ path: '/content-panel/tags', name: 'Tags' },
];

return (
<nav className={`${styles.navigation} ${isSun && styles.lightNavigation}`}>
<nav className={`${styles.navigation} ${isSun ? styles.lightNavigation : ''}`}>
<div className={styles.navigationContainer}>
<Link className={styles.navigationLogoLink} href="/">
<Image src={LogoNavigation} className={styles.navigationLogo} alt="LearnHub Logo" />
</Link>
<div className={styles.toggleContainer} onClick={toggleTheme}>
{isContentPanel && session?.user && (
<>
<div className={styles.navLinks}>
{contentPanelRoutes.map((route) => (
<Link
key={route.path}
href={route.path}
className={pathname === route.path ? styles.active : ''}
>
{route.name}
</Link>
))}
</div>
<div className={styles.rightSection}>
<div className={styles.avatarWrapper} ref={menuRef}>
<div
className={styles.avatarContainer}
onClick={() => setIsMenuOpen(!isMenuOpen)}
onKeyDown={handleKeyDown(() => setIsMenuOpen(!isMenuOpen))}
role="button"
tabIndex={0}
>
<Image
src={getImageSrc(user.image)}
alt="User avatar"
width={40}
height={40}
className={styles.avatar}
/>
{isMenuOpen ? (
<FaChevronUp className={styles.chevronIcon} />
) : (
<FaChevronDown className={styles.chevronIcon} />
)}
</div>
{isMenuOpen && renderAvatarMenu()}
</div>
</div>
</>
)}
<div
className={styles.toggleContainer}
onClick={toggleTheme}
onKeyDown={handleKeyDown(toggleTheme)}
role="button"
tabIndex={0}
>
<div className={`${styles.animate} ${!isSun ? styles.moveRight : ''}`}>
<div className={`${styles.toggleButton} ${isSun && styles.marginLeftButton}`} />
</div>

{isSun ? (
<Image
src={moonImage}
alt="moon icon"
width={18}
height={18}
className={styles.moonImage}
/>
) : (
<Image
src={sunImage}
alt="sun icon"
width={18}
height={18}
className={styles.sunImage}
/>
)}
<Image
src={isSun ? moonImage : sunImage}
alt={`${isSun ? 'moon' : 'sun'} icon`}
width={18}
height={18}
className={isSun ? styles.moonImage : styles.sunImage}
/>
</div>
</div>
</nav>
Expand Down
Loading