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
|
|
@ -7,8 +7,8 @@ import Button from '@/components/objects/Button'
|
||||||
import FeaturedRepos from '@/components/widgets/FeaturedRepos'
|
import FeaturedRepos from '@/components/widgets/FeaturedRepos'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { User } from 'lucide-react'
|
|
||||||
import { SiGoogle } from 'react-icons/si'
|
import { SiGoogle } from 'react-icons/si'
|
||||||
|
import { TbUserHeart } from 'react-icons/tb'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default function About() {
|
||||||
<main className="w-full">
|
<main className="w-full">
|
||||||
<div className="my-12 text-center">
|
<div className="my-12 text-center">
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-6">
|
||||||
<User size={60} />
|
<TbUserHeart size={60} />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">{t('about.title')}</h1>
|
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">{t('about.title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-gray-800 text-gray-400 py-4">
|
<footer className="bg-gray-800 text-gray-400 py-4">
|
||||||
<div className="flex flex-col sm:flex-row container mx-auto px-4 text-center items-center justify-center">
|
<div className="flex flex-col sm:flex-row container mx-auto px-4 text-center items-center justify-center">
|
||||||
<Link href="https://git.pontusmail.org/aidan/aidxnCC" target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors mb-2 sm:mb-0">
|
<Link href="https://git.p0ntus.com/aidan/aidxnCC" target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors mb-2 sm:mb-0">
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<TbCopyrightOff className="text-md mr-2" />
|
<TbCopyrightOff className="text-md mr-2" />
|
||||||
Open Source and Copyright-Free
|
Open Source and Copyright-Free
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
Brain,
|
Brain,
|
||||||
Smartphone
|
Smartphone
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { TbUserHeart } from 'react-icons/tb'
|
||||||
import { SiClaude, SiGoogle } from 'react-icons/si'
|
import { SiClaude, SiGoogle } from 'react-icons/si'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
|
@ -56,8 +57,8 @@ const DropdownNavItem = ({ id, href, icon, children, dropdownContent, isMobile =
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isMobile && isOpen) {
|
if (isMobile && isOpen) {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('click', handleClickOutside);
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('click', handleClickOutside);
|
||||||
}
|
}
|
||||||
}, [isMobile, isOpen, onOpenChange]);
|
}, [isMobile, isOpen, onOpenChange]);
|
||||||
|
|
||||||
|
|
@ -80,6 +81,7 @@ const DropdownNavItem = ({ id, href, icon, children, dropdownContent, isMobile =
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
onOpenChange?.(isOpen ? null : id);
|
onOpenChange?.(isOpen ? null : id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -94,10 +96,12 @@ const DropdownNavItem = ({ id, href, icon, children, dropdownContent, isMobile =
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
onClick={isMobile ? handleClick : undefined}
|
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"
|
||||||
>
|
>
|
||||||
|
<span className="flex items-center flex-1">
|
||||||
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
|
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
|
||||||
<span className="flex-1">{children}</span>
|
<span>{children}</span>
|
||||||
|
</span>
|
||||||
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
|
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
|
||||||
</Link>
|
</Link>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
|
@ -109,7 +113,7 @@ const DropdownNavItem = ({ id, href, icon, children, dropdownContent, isMobile =
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
isMobile
|
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'
|
: 'absolute left-0 mt-1 z-50 flex'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -130,9 +134,11 @@ interface NestedDropdownItemProps {
|
||||||
const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: NestedDropdownItemProps) => {
|
const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: NestedDropdownItemProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const itemRef = useRef<HTMLDivElement>(null);
|
const itemRef = useRef<HTMLDivElement>(null);
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -143,13 +149,14 @@ const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: Neste
|
||||||
if (relatedTarget && itemRef.current?.contains(relatedTarget)) {
|
if (relatedTarget && itemRef.current?.contains(relatedTarget)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsOpen(false);
|
timeoutRef.current = setTimeout(() => setIsOpen(false), 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -162,16 +169,16 @@ const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: Neste
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={handleClick}
|
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} />
|
<Smartphone className="mr-3" strokeWidth={2.5} size={18} />
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</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>
|
</button>
|
||||||
{isOpen && (
|
{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}
|
{nestedContent}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -188,13 +195,13 @@ const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: Neste
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={handleClick}
|
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} />
|
<Smartphone className="mr-3" strokeWidth={2.5} size={18} />
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</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>
|
</button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -229,8 +236,8 @@ const LanguageSelector = () => {
|
||||||
return () => window.removeEventListener('resize', checkMobile);
|
return () => window.removeEventListener('resize', checkMobile);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const changeLanguage = (lng: string) => {
|
const changeLanguage = async (lng: string) => {
|
||||||
i18n.changeLanguage(lng);
|
await i18n.changeLanguage(lng);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -288,12 +295,14 @@ const LanguageSelector = () => {
|
||||||
<button
|
<button
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onKeyDown={handleKeyDown}
|
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-expanded={isOpen}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
>
|
>
|
||||||
|
<span className="flex items-center flex-1">
|
||||||
<Globe className="text-md mr-2" strokeWidth={2.5} size={20} />
|
<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>{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} />
|
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
|
||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
|
@ -305,7 +314,7 @@ const LanguageSelector = () => {
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
isMobile
|
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'
|
: 'absolute right-0 mt-2 w-56 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50'
|
||||||
}`}
|
}`}
|
||||||
role="menu"
|
role="menu"
|
||||||
|
|
@ -316,10 +325,10 @@ const LanguageSelector = () => {
|
||||||
<button
|
<button
|
||||||
key={lang.code}
|
key={lang.code}
|
||||||
onClick={() => changeLanguage(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
|
i18n.language === lang.code
|
||||||
? 'text-white bg-gray-700'
|
? 'text-white bg-gray-700/50'
|
||||||
: 'text-gray-300 hover:text-white hover:bg-gray-700'
|
: 'text-gray-300 hover:text-white hover:bg-gray-700/50'
|
||||||
} transition-all duration-300`}
|
} transition-all duration-300`}
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
|
|
@ -338,7 +347,16 @@ export default function Header() {
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
|
|
@ -352,24 +370,24 @@ export default function Header() {
|
||||||
|
|
||||||
const aboutDropdownContent = (
|
const aboutDropdownContent = (
|
||||||
<>
|
<>
|
||||||
<div className="w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700">
|
<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-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
<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`}>
|
||||||
<User className="mr-3" strokeWidth={2.5} size={18} />
|
<TbUserHeart className="mr-3" size={18} />
|
||||||
About Me
|
Get to Know Me
|
||||||
</Link>
|
</Link>
|
||||||
<NestedDropdownItem
|
<NestedDropdownItem
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
nestedContent={
|
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} />
|
<SiGoogle className="mr-3" size={18} />
|
||||||
Pixel 3a XL (bonito)
|
Pixel 3a XL (bonito)
|
||||||
</Link>
|
</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} />
|
<SiGoogle className="mr-3" size={18} />
|
||||||
Pixel 7 Pro (cheetah)
|
Pixel 7 Pro (cheetah)
|
||||||
</Link>
|
</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} />
|
<SiGoogle className="mr-3" size={18} />
|
||||||
Pixel 9 Pro (komodo)
|
Pixel 9 Pro (komodo)
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -383,12 +401,8 @@ export default function Header() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const aiDropdownContent = (
|
const aiDropdownContent = (
|
||||||
<div className="w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700">
|
<div className={`${isMobile ? 'w-full' : '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">
|
<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`}>
|
||||||
<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">
|
|
||||||
<SiClaude className="mr-3" size={18} />
|
<SiClaude className="mr-3" size={18} />
|
||||||
Claude Usage
|
Claude Usage
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -418,7 +432,7 @@ export default function Header() {
|
||||||
<button onClick={toggleMenu} className="lg:hidden text-gray-300 focus:outline-hidden">
|
<button onClick={toggleMenu} className="lg:hidden text-gray-300 focus:outline-hidden">
|
||||||
{isOpen ? <X className="text-2xl" /> : <Menu className="text-2xl" />}
|
{isOpen ? <X className="text-2xl" /> : <Menu className="text-2xl" />}
|
||||||
</button>
|
</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>
|
<NavItem href="/" icon={House}>Home</NavItem>
|
||||||
<DropdownNavItem
|
<DropdownNavItem
|
||||||
id="about"
|
id="about"
|
||||||
|
|
@ -427,9 +441,9 @@ export default function Header() {
|
||||||
dropdownContent={aboutDropdownContent}
|
dropdownContent={aboutDropdownContent}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
isOpen={activeDropdown === 'about'}
|
isOpen={activeDropdown === 'about'}
|
||||||
onOpenChange={setActiveDropdown}
|
onOpenChange={handleDropdownChange}
|
||||||
>
|
>
|
||||||
About
|
About Me
|
||||||
</DropdownNavItem>
|
</DropdownNavItem>
|
||||||
<DropdownNavItem
|
<DropdownNavItem
|
||||||
id="ai"
|
id="ai"
|
||||||
|
|
@ -438,14 +452,14 @@ export default function Header() {
|
||||||
dropdownContent={aiDropdownContent}
|
dropdownContent={aiDropdownContent}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
isOpen={activeDropdown === 'ai'}
|
isOpen={activeDropdown === 'ai'}
|
||||||
onOpenChange={setActiveDropdown}
|
onOpenChange={handleDropdownChange}
|
||||||
>
|
>
|
||||||
AI
|
AI
|
||||||
</DropdownNavItem>
|
</DropdownNavItem>
|
||||||
<NavItem href="/contact" icon={Phone}>Contact</NavItem>
|
<NavItem href="/contact" icon={Phone}>Contact</NavItem>
|
||||||
<NavItem href="/domains" icon={LinkIcon}>Domains</NavItem>
|
<NavItem href="/domains" icon={LinkIcon}>Domains</NavItem>
|
||||||
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</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 />
|
<LanguageSelector />
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ const NowPlaying: React.FC = () => {
|
||||||
const [track, setTrack] = useState<Track | null>(null)
|
const [track, setTrack] = useState<Track | null>(null)
|
||||||
const [coverArt, setCoverArt] = useState<string | null>(null)
|
const [coverArt, setCoverArt] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [albumArtLoading, setAlbumArtLoading] = useState(false)
|
||||||
const [loadingStatus, setLoadingStatus] = useState("Initializing")
|
const [loadingStatus, setLoadingStatus] = useState("Initializing")
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [progress, setProgress] = useState(0)
|
const [progress, setProgress] = useState(0)
|
||||||
|
|
@ -43,10 +44,11 @@ const NowPlaying: React.FC = () => {
|
||||||
if (!album) {
|
if (!album) {
|
||||||
updateProgress(0, 0, "No album found")
|
updateProgress(0, 0, "No album found")
|
||||||
setCoverArt(null)
|
setCoverArt(null)
|
||||||
setLoading(false)
|
setAlbumArtLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
setAlbumArtLoading(true)
|
||||||
updateProgress(2, 3, `Searching for album: ${artist} - ${album}`)
|
updateProgress(2, 3, `Searching for album: ${artist} - ${album}`)
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://musicbrainz.org/ws/2/release/?query=artist:${encodeURIComponent(
|
`https://musicbrainz.org/ws/2/release/?query=artist:${encodeURIComponent(
|
||||||
|
|
@ -56,7 +58,7 @@ const NowPlaying: React.FC = () => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
updateProgress(0, 0, `Album art fetch error: ${response.status}`)
|
updateProgress(0, 0, `Album art fetch error: ${response.status}`)
|
||||||
setError("Error fetching album art (see console for details)")
|
setError("Error fetching album art (see console for details)")
|
||||||
setLoading(false)
|
setAlbumArtLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
@ -67,21 +69,21 @@ const NowPlaying: React.FC = () => {
|
||||||
const coverArtResponse = await fetch(`https://coverartarchive.org/release/${mbid}/front`)
|
const coverArtResponse = await fetch(`https://coverartarchive.org/release/${mbid}/front`)
|
||||||
if (coverArtResponse.ok) {
|
if (coverArtResponse.ok) {
|
||||||
setCoverArt(coverArtResponse.url)
|
setCoverArt(coverArtResponse.url)
|
||||||
setLoading(false)
|
setAlbumArtLoading(false)
|
||||||
} else {
|
} else {
|
||||||
updateProgress(0, 0, "Cover art not found")
|
updateProgress(0, 0, "Cover art not found")
|
||||||
setCoverArt(null)
|
setCoverArt(null)
|
||||||
setLoading(false)
|
setAlbumArtLoading(false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateProgress(0, 0, "No releases found")
|
updateProgress(0, 0, "No releases found")
|
||||||
setCoverArt(null)
|
setCoverArt(null)
|
||||||
setLoading(false)
|
setAlbumArtLoading(false)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
updateProgress(0, 0, `Error: ${error}`)
|
updateProgress(0, 0, `Error: ${error}`)
|
||||||
setCoverArt(null)
|
setCoverArt(null)
|
||||||
setLoading(false)
|
setAlbumArtLoading(false)
|
||||||
}
|
}
|
||||||
}, [updateProgress])
|
}, [updateProgress])
|
||||||
|
|
||||||
|
|
@ -103,6 +105,7 @@ const NowPlaying: React.FC = () => {
|
||||||
release_name: trackMetadata.release_name,
|
release_name: trackMetadata.release_name,
|
||||||
mbid: trackMetadata.mbid,
|
mbid: trackMetadata.mbid,
|
||||||
})
|
})
|
||||||
|
setLoading(false)
|
||||||
updateProgress(2, 3, "Finding album art...")
|
updateProgress(2, 3, "Finding album art...")
|
||||||
await fetchAlbumArt(trackMetadata.artist_name, trackMetadata.release_name)
|
await fetchAlbumArt(trackMetadata.artist_name, trackMetadata.release_name)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -184,12 +187,17 @@ const NowPlaying: React.FC = () => {
|
||||||
<div className="text-center leading-none pb-1">
|
<div className="text-center leading-none pb-1">
|
||||||
<ScrollTxt text={currentTrack.artist_name.toUpperCase()} type="artist" />
|
<ScrollTxt text={currentTrack.artist_name.toUpperCase()} type="artist" />
|
||||||
<ScrollTxt text={currentTrack.track_name} type="track" className="-mt-0.5" />
|
<ScrollTxt text={currentTrack.track_name} type="track" className="-mt-0.5" />
|
||||||
{currentTrack.release_name && <ScrollTxt text={currentTrack.release_name} type="release" />}
|
{currentTrack.release_name && <ScrollTxt text={currentTrack.release_name} type="release" className="-mt-1.5" />}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{/* Album art */}
|
{/* Album art */}
|
||||||
<div className="relative w-full aspect-square">
|
<div className="relative w-full aspect-square">
|
||||||
{coverArt ? (
|
{albumArtLoading ? (
|
||||||
|
<div className="w-full h-full bg-gray-700 flex flex-col items-center justify-center">
|
||||||
|
<Loader2 className="animate-spin text-gray-400 mb-2" size={32} />
|
||||||
|
<div className="text-gray-400 text-xs">Fetching Album Art</div>
|
||||||
|
</div>
|
||||||
|
) : coverArt ? (
|
||||||
<Image
|
<Image
|
||||||
src={coverArt}
|
src={coverArt}
|
||||||
alt={currentTrack.track_name}
|
alt={currentTrack.track_name}
|
||||||
|
|
@ -208,7 +216,7 @@ const NowPlaying: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-center items-center">
|
||||||
<div className="relative w-52 h-95 bg-[#D4C29A] rounded-xs border border-[#BFAF8A] z-10">
|
<div className={`relative w-52 bg-[#D4C29A] rounded-xs border border-[#BFAF8A] z-10 ${track?.release_name ? "h-[24.25rem]" : "h-[23.6rem]"}`}>
|
||||||
{/* Volume buttons */}
|
{/* Volume buttons */}
|
||||||
<div className="absolute -left-[2.55px] top-8 rounded-l w-[1.75px] flex flex-col z-0">
|
<div className="absolute -left-[2.55px] top-8 rounded-l w-[1.75px] flex flex-col z-0">
|
||||||
<div className="h-8 bg-[#BFAF8A] border-b border-[#A09070] rounded-l cursor-pointer" onClick={() => setVolume(v => Math.min(100, v + 5))}></div> {/* up */}
|
<div className="h-8 bg-[#BFAF8A] border-b border-[#A09070] rounded-l cursor-pointer" onClick={() => setVolume(v => Math.min(100, v + 5))}></div> {/* up */}
|
||||||
|
|
@ -238,7 +246,7 @@ const NowPlaying: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
{/* Player controls and seekbar */}
|
{/* Player controls and seekbar */}
|
||||||
{screenOn && track && (
|
{screenOn && track && (
|
||||||
<div className="bg-gradient-to-b from-gray-700 to-gray-900 pb-2.5 flex flex-col items-center" style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}}>
|
<div className={`bg-gradient-to-b from-gray-700 to-gray-900 ${track?.release_name ? "pb-3" : "pb-[12.5px]"} flex flex-col items-center`} style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}}>
|
||||||
<div className="flex justify-center items-center gap-0 px-2">
|
<div className="flex justify-center items-center gap-0 px-2">
|
||||||
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
|
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
|
||||||
<svg width="38" height="34" viewBox="0 0 24 20" className="drop-shadow-sm">
|
<svg width="38" height="34" viewBox="0 0 24 20" className="drop-shadow-sm">
|
||||||
|
|
|
||||||
12
package.json
12
package.json
|
|
@ -12,7 +12,7 @@
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"geist": "^1.4.2",
|
"geist": "^1.5.1",
|
||||||
"i18next": "^24.2.3",
|
"i18next": "^24.2.3",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"lucide-react": "^0.485.0",
|
"lucide-react": "^0.485.0",
|
||||||
|
|
@ -24,18 +24,18 @@
|
||||||
"recharts": "^3.1.2",
|
"recharts": "^3.1.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tw-animate-css": "^1.3.7"
|
"tw-animate-css": "^1.3.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@tailwindcss/postcss": "^4.1.12",
|
"@tailwindcss/postcss": "^4.1.13",
|
||||||
"@types/node": "^20.19.11",
|
"@types/node": "^20.19.13",
|
||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.35.0",
|
||||||
"eslint-config-next": "15.1.3",
|
"eslint-config-next": "15.1.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.12",
|
"tailwindcss": "^4.1.13",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"title": "About Me",
|
"title": "Get to Know Me",
|
||||||
"description": "Hey there! My name is Aidan, and I'm a systems administrator, full-stack developer, and student from the United States. I primarily work with Linux, Docker, Next.js, and Node.js.",
|
"description": "Hey there! My name is Aidan, and I'm a systems administrator, full-stack developer, and student from the United States. I primarily work with Linux, Docker, Next.js, and Node.js.",
|
||||||
"sections": {
|
"sections": {
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue