import React, { useRef, useState, useEffect, useCallback } from 'react'; import { NavLink, Link, useLocation, useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { CaretDoubleDownIcon, CaretDoubleUpIcon, ArrowRightIcon, MagnifyingGlassIcon, GearSixIcon, ShuffleIcon, EnvelopeSimpleIcon, } from '@phosphor-icons/react'; import { version } from '../version'; import usePersistentState from '../hooks/usePersistentState'; import { KEY_SIDEBAR_STATE } from '../utils/LocalStorageManager'; import { useAchievements } from '../context/AchievementContext'; import { appIcons as ICON_MAP } from '../utils/appIcons'; import piml from 'piml'; const LuxeSidebar = ({ isOpen, toggleSidebar, toggleModal, setIsPaletteOpen, }) => { const [sidebarConfig, setSidebarConfig] = useState(null); useEffect(() => { const fetchSidebarConfig = async () => { try { const response = await fetch('/sidebar.piml'); if (response.ok) { const text = await response.text(); const parsed = piml.parse(text); setSidebarConfig(parsed.sidebar); } } catch (error) { console.error('Failed to load sidebar config:', error); } }; fetchSidebarConfig(); }, []); const [sidebarState, setSidebarState] = usePersistentState( KEY_SIDEBAR_STATE, { isMainOpen: true, isContentOpen: true, isSoftwareOpen: true, isAppsOpen: true, isStatusOpen: false, isExtrasOpen: false, }, ); const { unlockAchievement } = useAchievements(); const scrollRef = useRef(null); const location = useLocation(); const navigate = useNavigate(); const [showScrollGradient, setShowScrollGradient] = useState({ top: false, bottom: false, }); const checkScroll = useCallback(() => { if (scrollRef.current) { const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; const atBottom = Math.ceil(scrollTop + clientHeight) >= scrollHeight; const atTop = scrollTop <= 0; const isScrollable = scrollHeight > clientHeight; setShowScrollGradient({ top: isScrollable && !atTop, bottom: isScrollable && !atBottom, }); } }, []); useEffect(() => { checkScroll(); const scrollElement = scrollRef.current; if (scrollElement) { scrollElement.addEventListener('scroll', checkScroll); window.addEventListener('resize', checkScroll); } return () => { if (scrollElement) { scrollElement.removeEventListener('scroll', checkScroll); window.removeEventListener('resize', checkScroll); } }; }, [isOpen, sidebarState, checkScroll]); const toggleSection = (section) => { setSidebarState((prevState) => ({ ...prevState, [section]: !prevState[section], })); }; const getLinkClass = ({ isActive }) => `group flex items-center justify-between px-8 py-3 transition-all duration-500 border-l-2 ${ isActive ? 'border-[#8D4004] bg-[#8D4004]/5 text-[#1A1A1A]' : 'border-transparent text-[#1A1A1A]/60 hover:text-[#1A1A1A] hover:bg-[#1A1A1A]/5' }`; const SectionHeader = ({ id, label, isOpen, active }) => ( ); const sidebarVariants = { open: { x: 0, transition: { type: 'spring', stiffness: 300, damping: 30 } }, closed: { x: '-100%', transition: { type: 'spring', stiffness: 300, damping: 30 }, }, }; return ( <>
Fezcodex Version v{version}
{/* Scrollable Content */}
{showScrollGradient.top && (
)}
{Array.isArray(sidebarConfig) && sidebarConfig.map((section, sectionIdx) => { const items = Array.isArray(section.content) ? section.content : []; const isActive = items.some((item) => item.to === '/' ? location.pathname === '/' : item.to && location.pathname.startsWith(item.to), ); return ( {sidebarState[section.id] && ( {items.map((item, idx) => { const Icon = ICON_MAP[item.icon] || ArrowRightIcon; if (item.external === 'true' || item.url || (item.to && item.to.startsWith('http'))) { /* Use native tag for external absolute URLs to bypass React Router. This allows the browser to perform a normal page navigation instead of React Router attempting to resolve it internally which causes a 404. */ return (
{item.label}
); } return ( ); })}
)}
); })}
{showScrollGradient.bottom && (
)}
setIsPaletteOpen(true)} icon={MagnifyingGlassIcon} title="Search" /> navigate('/settings')} icon={GearSixIcon} title="Config" /> { navigate('/random'); unlockAchievement('feeling_lucky'); }} icon={ShuffleIcon} title="Random" />
); }; const SidebarLink = ({ to, icon: Icon, label, getLinkClass }) => (
{Icon && } {label}
); const FooterButton = ({ onClick, icon: Icon, title }) => ( ); export default LuxeSidebar;