feat/fix: improve state management and design of NowPlaying component, bump, improve dropdown menus and re-organize, rename About page, fix footer link
This commit is contained in:
parent
9edf78595d
commit
4cec7406c3
6 changed files with 84 additions and 62 deletions
|
|
@ -16,6 +16,7 @@ import {
|
|||
Brain,
|
||||
Smartphone
|
||||
} from 'lucide-react'
|
||||
import { TbUserHeart } from 'react-icons/tb'
|
||||
import { SiClaude, SiGoogle } from 'react-icons/si'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
|
|
@ -56,8 +57,8 @@ const DropdownNavItem = ({ id, href, icon, children, dropdownContent, isMobile =
|
|||
};
|
||||
|
||||
if (isMobile && isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}
|
||||
}, [isMobile, isOpen, onOpenChange]);
|
||||
|
||||
|
|
@ -80,6 +81,7 @@ const DropdownNavItem = ({ id, href, icon, children, dropdownContent, isMobile =
|
|||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (isMobile) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenChange?.(isOpen ? null : id);
|
||||
}
|
||||
};
|
||||
|
|
@ -94,10 +96,12 @@ const DropdownNavItem = ({ id, href, icon, children, dropdownContent, isMobile =
|
|||
<Link
|
||||
href={href}
|
||||
onClick={isMobile ? handleClick : undefined}
|
||||
className="flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 w-full"
|
||||
className="flex items-center justify-between text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 w-full"
|
||||
>
|
||||
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
|
||||
<span className="flex-1">{children}</span>
|
||||
<span className="flex items-center flex-1">
|
||||
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
|
||||
</Link>
|
||||
{isOpen && (
|
||||
|
|
@ -109,7 +113,7 @@ const DropdownNavItem = ({ id, href, icon, children, dropdownContent, isMobile =
|
|||
<div
|
||||
className={`${
|
||||
isMobile
|
||||
? 'relative mt-2 w-full bg-gray-700/50 rounded-md'
|
||||
? 'relative w-full mt-2 ml-5 pr-4'
|
||||
: 'absolute left-0 mt-1 z-50 flex'
|
||||
}`}
|
||||
>
|
||||
|
|
@ -130,9 +134,11 @@ interface NestedDropdownItemProps {
|
|||
const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: NestedDropdownItemProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const itemRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isMobile) {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
setIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
|
@ -143,13 +149,14 @@ const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: Neste
|
|||
if (relatedTarget && itemRef.current?.contains(relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(false);
|
||||
timeoutRef.current = setTimeout(() => setIsOpen(false), 100);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (isMobile) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
};
|
||||
|
|
@ -162,16 +169,16 @@ const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: Neste
|
|||
>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="flex items-center justify-between w-full text-left px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300"
|
||||
className="flex items-center justify-between w-full text-left px-4 py-3 text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="flex items-center flex-1">
|
||||
<Smartphone className="mr-3" strokeWidth={2.5} size={18} />
|
||||
{children}
|
||||
</span>
|
||||
<ChevronRight className={`transform transition-transform duration-200 ${isOpen ? '-rotate-90' : ''}`} strokeWidth={2.5} size={18} />
|
||||
<ChevronRight className={`transform transition-transform duration-200 ${isOpen ? 'rotate-90' : ''}`} strokeWidth={2.5} size={18} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="relative mt-2 ml-4 bg-gray-700/30 rounded-md">
|
||||
<div className="relative mt-2 ml-5 pr-4 space-y-1">
|
||||
{nestedContent}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -188,13 +195,13 @@ const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: Neste
|
|||
>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="flex items-center justify-between w-full text-left px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300"
|
||||
className="flex items-center justify-between w-full text-left px-4 py-3 text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="flex items-center flex-1">
|
||||
<Smartphone className="mr-3" strokeWidth={2.5} size={18} />
|
||||
{children}
|
||||
</span>
|
||||
<ChevronRight className={`transform transition-transform duration-200 ${isOpen ? 'rotate-0' : 'rotate-90'}`} strokeWidth={2.5} size={18} />
|
||||
<ChevronDown className={`transform transition-transform duration-200 ${isOpen ? '-rotate-90' : ''}`} strokeWidth={2.5} size={18} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<>
|
||||
|
|
@ -229,8 +236,8 @@ const LanguageSelector = () => {
|
|||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
i18n.changeLanguage(lng);
|
||||
const changeLanguage = async (lng: string) => {
|
||||
await i18n.changeLanguage(lng);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
|
|
@ -288,12 +295,14 @@ const LanguageSelector = () => {
|
|||
<button
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={`flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 ${isMobile ? 'w-full' : ''}`}
|
||||
className={`flex items-center ${isMobile ? 'justify-between' : ''} text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 ${isMobile ? 'w-full' : ''}`}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<Globe className="text-md mr-2" strokeWidth={2.5} size={20} />
|
||||
<span className="flex-1">{languages.find(lang => lang.code === i18n.language)?.name || 'English'}</span>
|
||||
<span className="flex items-center flex-1">
|
||||
<Globe className="text-md mr-2" strokeWidth={2.5} size={20} />
|
||||
<span>{languages.find(lang => lang.code === i18n.language)?.name || 'English'}</span>
|
||||
</span>
|
||||
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
|
|
@ -305,7 +314,7 @@ const LanguageSelector = () => {
|
|||
<div
|
||||
className={`${
|
||||
isMobile
|
||||
? 'relative mt-2 w-full bg-gray-700/50 rounded-md'
|
||||
? 'relative w-full mt-2 ml-4 pr-4 space-y-1'
|
||||
: 'absolute right-0 mt-2 w-56 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50'
|
||||
}`}
|
||||
role="menu"
|
||||
|
|
@ -316,10 +325,10 @@ const LanguageSelector = () => {
|
|||
<button
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
className={`block w-full text-left px-5 py-3 text-base rounded-md ${
|
||||
className={`block w-full text-left ${isMobile ? 'px-4 py-2.5' : 'px-5 py-3'} ${isMobile ? 'text-sm' : 'text-base'} rounded-md ${
|
||||
i18n.language === lang.code
|
||||
? 'text-white bg-gray-700'
|
||||
: 'text-gray-300 hover:text-white hover:bg-gray-700'
|
||||
? 'text-white bg-gray-700/50'
|
||||
: 'text-gray-300 hover:text-white hover:bg-gray-700/50'
|
||||
} transition-all duration-300`}
|
||||
role="menuitem"
|
||||
>
|
||||
|
|
@ -338,7 +347,16 @@ export default function Header() {
|
|||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||
|
||||
const toggleMenu = () => setIsOpen(!isOpen);
|
||||
const toggleMenu = () => {
|
||||
setIsOpen(!isOpen);
|
||||
if (isOpen) {
|
||||
setActiveDropdown(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDropdownChange = (id: string | null) => {
|
||||
setActiveDropdown(id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
|
|
@ -352,24 +370,24 @@ export default function Header() {
|
|||
|
||||
const aboutDropdownContent = (
|
||||
<>
|
||||
<div className="w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700">
|
||||
<Link href="/about" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||
<User className="mr-3" strokeWidth={2.5} size={18} />
|
||||
About Me
|
||||
<div className={`${isMobile ? 'w-full' : 'w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700'}`}>
|
||||
<Link href="/about" className={`flex items-center px-4 py-3 text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300 cursor-pointer`}>
|
||||
<TbUserHeart className="mr-3" size={18} />
|
||||
Get to Know Me
|
||||
</Link>
|
||||
<NestedDropdownItem
|
||||
isMobile={isMobile}
|
||||
nestedContent={
|
||||
<>
|
||||
<Link href="/device/bonito" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||
<Link href="/device/bonito" className={`flex items-center ${isMobile ? 'px-4 py-3' : 'px-5 py-3'} text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300`}>
|
||||
<SiGoogle className="mr-3" size={18} />
|
||||
Pixel 3a XL (bonito)
|
||||
</Link>
|
||||
<Link href="/device/cheetah" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||
<Link href="/device/cheetah" className={`flex items-center ${isMobile ? 'px-4 py-3' : 'px-5 py-3'} text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300`}>
|
||||
<SiGoogle className="mr-3" size={18} />
|
||||
Pixel 7 Pro (cheetah)
|
||||
</Link>
|
||||
<Link href="/device/komodo" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||
<Link href="/device/komodo" className={`flex items-center ${isMobile ? 'px-4 py-3' : 'px-5 py-3'} text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300`}>
|
||||
<SiGoogle className="mr-3" size={18} />
|
||||
Pixel 9 Pro (komodo)
|
||||
</Link>
|
||||
|
|
@ -383,12 +401,8 @@ export default function Header() {
|
|||
);
|
||||
|
||||
const aiDropdownContent = (
|
||||
<div className="w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700">
|
||||
<Link href="/ai" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||
<Brain className="mr-3" strokeWidth={2.5} size={18} />
|
||||
AI
|
||||
</Link>
|
||||
<Link href="/ai/claude" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||
<div className={`${isMobile ? 'w-full' : 'w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700'}`}>
|
||||
<Link href="/ai/claude" className={`flex items-center px-4 py-3 text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300`}>
|
||||
<SiClaude className="mr-3" size={18} />
|
||||
Claude Usage
|
||||
</Link>
|
||||
|
|
@ -418,7 +432,7 @@ export default function Header() {
|
|||
<button onClick={toggleMenu} className="lg:hidden text-gray-300 focus:outline-hidden">
|
||||
{isOpen ? <X className="text-2xl" /> : <Menu className="text-2xl" />}
|
||||
</button>
|
||||
<ul className={`flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-4 absolute lg:static bg-gray-800 lg:bg-transparent w-full lg:w-auto left-0 lg:left-auto top-full lg:top-auto p-4 lg:p-0 transition-all duration-300 ease-in-out z-50 ${isOpen ? 'flex' : 'hidden lg:flex'}`}>
|
||||
<ul className={`flex flex-col lg:flex-row space-y-3 lg:space-y-0 lg:space-x-4 absolute lg:static bg-gray-800 lg:bg-transparent w-full lg:w-auto left-0 lg:left-auto top-full lg:top-auto px-2 py-4 lg:p-0 transition-all duration-300 ease-in-out z-50 ${isOpen ? 'flex' : 'hidden lg:flex'}`}>
|
||||
<NavItem href="/" icon={House}>Home</NavItem>
|
||||
<DropdownNavItem
|
||||
id="about"
|
||||
|
|
@ -427,9 +441,9 @@ export default function Header() {
|
|||
dropdownContent={aboutDropdownContent}
|
||||
isMobile={isMobile}
|
||||
isOpen={activeDropdown === 'about'}
|
||||
onOpenChange={setActiveDropdown}
|
||||
onOpenChange={handleDropdownChange}
|
||||
>
|
||||
About
|
||||
About Me
|
||||
</DropdownNavItem>
|
||||
<DropdownNavItem
|
||||
id="ai"
|
||||
|
|
@ -438,14 +452,14 @@ export default function Header() {
|
|||
dropdownContent={aiDropdownContent}
|
||||
isMobile={isMobile}
|
||||
isOpen={activeDropdown === 'ai'}
|
||||
onOpenChange={setActiveDropdown}
|
||||
onOpenChange={handleDropdownChange}
|
||||
>
|
||||
AI
|
||||
</DropdownNavItem>
|
||||
<NavItem href="/contact" icon={Phone}>Contact</NavItem>
|
||||
<NavItem href="/domains" icon={LinkIcon}>Domains</NavItem>
|
||||
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem>
|
||||
<div className="lg:hidden">
|
||||
<div className="lg:hidden mt-2 pt-3 -mb-1.5 border-t border-gray-600/30">
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
</ul>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue