source-code/
portofolio-neo-gruv
Public
codeCodeinfoIssues 0call_splitPull Requestsplay_circleActions
portofolio-neo-gruv/src/layout/Navbar.tsx
typescript200 lines10.2 KB
"use client";

import React, { useState, useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { Button } from '../ui/Button';
import { useData } from '../context/DataContext';

export const Navbar: React.FC = () => {
    const pathname = usePathname();
    const router = useRouter();
    const isSourceCode = pathname?.startsWith('/source-code/');
    
    // Extract repo name if in source code
    let repoName = '';
    if (isSourceCode && pathname) {
        const segments = pathname.split('/');
        if (segments.length > 2) {
            repoName = segments[2];
        }
    }

    const [floating, setFloating] = useState(false);
    const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
    const { navigationData, marqueeItems } = useData();
    const { brandName, navLinks, ctaText } = navigationData;

    useEffect(() => {
        const onScroll = () => setFloating(window.scrollY > 50);
        window.addEventListener('scroll', onScroll, { passive: true });
        return () => window.removeEventListener('scroll', onScroll);
    }, []);

    useEffect(() => {
        if (isMobileMenuOpen) {
            document.body.style.overflow = 'hidden';
        } else {
            document.body.style.overflow = 'auto';
        }
    }, [isMobileMenuOpen]);

    const handleMobileNav = (id: string) => {
        setIsMobileMenuOpen(false);
        const element = id.startsWith('#') ? document.getElementById(id.substring(1)) : document.getElementById(id);
        if (element) {
            element.scrollIntoView({ behavior: 'smooth' });
        }
    };

    if (isSourceCode) {
        return (
            <nav
                className={[
                    'sticky top-0 z-[110] flex justify-between items-center px-gutter text-primary',
                    'transition-all duration-300 ease-in-out',
                    floating
                        ? 'mx-4 mt-5 h-16 rounded-2xl border-[3px] border-on-surface bg-background/90 backdrop-blur-md shadow-[4px_4px_0px_0px_#1e1b19]'
                        : 'w-full h-24 border-b-[4px] border-on-surface bg-background shadow-[8px_8px_0px_0px_#1e1b19]',
                ].join(' ')}
            >
                <div 
                    onClick={() => router.push('/')}
                    className={[
                        'font-display-2xl font-extrabold uppercase tracking-tighter text-on-surface transition-all duration-300 relative z-[120] cursor-pointer',
                        floating ? 'text-[24px]' : 'text-[32px]',
                    ].join(' ')}
                >
                    {brandName}
                </div>
                <div className="flex items-center gap-3 md:gap-4 z-[120]">
                    <Button 
                        onClick={() => router.push('/')}
                        className="bg-surface text-on-surface neo-border neo-shadow px-3 py-1.5 text-xs md:text-sm md:px-6 md:py-2 hover:bg-theme-red hover:text-surface-container-lowest hover:translate-x-1 hover:translate-y-1 hover:shadow-[4px_4px_0px_0px_#1e1b19]"
                    >
                        BACK TO PORTFOLIO
                    </Button>
                    <Button 
                        onClick={() => window.open(`https://github.com/andreyyste/${repoName}`, '_blank')}
                        className="bg-theme-yellow text-on-surface neo-border neo-shadow px-3 py-1.5 text-xs md:text-sm md:px-6 md:py-2 hover:bg-theme-blue hover:text-surface-container-lowest hover:translate-x-1 hover:translate-y-1 hover:shadow-[4px_4px_0px_0px_#1e1b19]"
                    >
                        OPEN ON GITHUB
                    </Button>
                </div>
            </nav>
        );
    }

    return (
        <>
            {/* Thin Black Marquee (Mobile Only) */}
            <div className="w-full overflow-hidden bg-on-surface text-surface py-2 border-b-[3px] border-on-surface md:hidden z-[115] relative">
                <div className="flex w-max animate-[marquee_200s_linear_infinite] font-label-bold text-[10px] uppercase whitespace-nowrap">
                    <div className="flex gap-8 pr-8">
                        {Array.from({ length: 5 }).flatMap(() => marqueeItems || []).map((item, i) => (
                            <span key={`g1-${i}`}>{item}</span>
                        ))}
                    </div>
                    <div className="flex gap-8 pr-8">
                        {Array.from({ length: 5 }).flatMap(() => marqueeItems || []).map((item, i) => (
                            <span key={`g2-${i}`}>{item}</span>
                        ))}
                    </div>
                </div>
            </div>

            <nav
                className={[
                    'sticky top-0 z-[110] flex justify-between items-center px-gutter text-primary',
                    'transition-all duration-300 ease-in-out',
                    floating
                        ? 'mx-4 mt-5 h-16 rounded-2xl border-[3px] border-on-surface bg-background/90 backdrop-blur-md shadow-[4px_4px_0px_0px_#1e1b19]'
                        : 'w-full h-16 md:h-24 border-b-[4px] border-on-surface bg-background md:shadow-[8px_8px_0px_0px_#1e1b19]',
                ].join(' ')}
            >
                <div className={[
                    'font-display-2xl font-extrabold uppercase tracking-tighter text-on-surface transition-all duration-300 relative z-[120]',
                    floating ? 'text-[24px]' : 'text-[24px] md:text-[32px]',
                ].join(' ')}>
                    {brandName}
                </div>
                <div className="hidden md:flex items-center gap-8">
                    {(navLinks || []).map((link) => (
                        <a key={link.href} className="text-on-surface font-label-bold text-label-bold uppercase hover:bg-theme-yellow hover:text-on-surface transition-colors duration-100 p-2 border-[4px] border-transparent hover:border-on-surface active:translate-x-1 active:translate-y-1" href={link.href}>{link.label}</a>
                    ))}
                </div>
                <div className="hidden md:flex items-center gap-4">
                    <Button 
                        onClick={() => router.push('/nre-masuk')}
                        className="bg-surface text-on-surface neo-border neo-shadow px-6 py-2 hover:bg-theme-red hover:text-surface-container-lowest hover:translate-x-1 hover:translate-y-1 hover:shadow-[4px_4px_0px_0px_#1e1b19]"
                    >
                        LOGIN
                    </Button>
                    <Button 
                        onClick={() => document.getElementById('contact')?.scrollIntoView({ behavior: 'smooth' })}
                        className="bg-theme-yellow text-on-surface neo-border neo-shadow px-6 py-2 hover:bg-theme-blue hover:text-surface-container-lowest hover:translate-x-1 hover:translate-y-1 hover:shadow-[4px_4px_0px_0px_#1e1b19]"
                    >
                        {ctaText}
                    </Button>
                </div>
                <button 
                    className="md:hidden text-on-surface relative z-[120]"
                    onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
                >
                    <span className="material-symbols-outlined text-4xl">
                        {isMobileMenuOpen ? 'close' : 'menu'}
                    </span>
                </button>
            </nav>
 
            {/* Mobile Menu Overlay */}
            <div 
                className={[
                    'fixed inset-0 bg-theme-blue z-[105] flex flex-col justify-center items-center transition-transform duration-500 ease-[cubic-bezier(0.4,0,0.2,1)]',
                    isMobileMenuOpen ? 'translate-y-0' : '-translate-y-full'
                ].join(' ')}
            >
                <div className="flex flex-col items-center gap-8 w-full px-8">
                    {(navLinks || []).map((link, index) => (
                        <button 
                            key={link.href} 
                            onClick={() => handleMobileNav(link.href)}
                            className="text-surface-container-lowest font-headline-lg-mobile text-[48px] uppercase font-bold hover:text-theme-yellow transition-colors duration-200"
                            style={{ 
                                transitionDelay: isMobileMenuOpen ? `${index * 100}ms` : '0ms',
                                opacity: isMobileMenuOpen ? 1 : 0,
                                transform: isMobileMenuOpen ? 'translateY(0)' : 'translateY(20px)',
                                transition: 'all 0.4s ease-out'
                            }}
                        >
                            {link.label}
                        </button>
                    ))}
                    <Button 
                        onClick={() => handleMobileNav('contact')}
                        className="mt-8 bg-theme-yellow text-on-surface neo-border-heavy px-8 py-4 text-2xl uppercase w-full max-w-[280px]"
                        style={{ 
                            transitionDelay: isMobileMenuOpen ? `${navLinks.length * 100}ms` : '0ms',
                            opacity: isMobileMenuOpen ? 1 : 0,
                            transform: isMobileMenuOpen ? 'translateY(0)' : 'translateY(20px)',
                            transition: 'all 0.4s ease-out'
                        }}
                    >
                        {ctaText}
                    </Button>
                    <Button 
                        onClick={() => router.push('/nre-masuk')}
                        className="mt-4 bg-transparent text-surface-container-lowest border-[4px] border-surface-container-lowest px-8 py-4 text-2xl uppercase w-full max-w-[280px] hover:bg-theme-red hover:border-theme-red transition-colors"
                        style={{ 
                            transitionDelay: isMobileMenuOpen ? `${(navLinks.length + 1) * 100}ms` : '0ms',
                            opacity: isMobileMenuOpen ? 1 : 0,
                            transform: isMobileMenuOpen ? 'translateY(0)' : 'translateY(20px)',
                            transition: 'all 0.4s ease-out'
                        }}
                    >
                        LOGIN
                    </Button>
                </div>
            </div>
        </>
    );
};

About

Custom portfolio frontend designed using retro Neo-Brutalist styling. Features server-rendered pages, persistent codebase layout, interactive file explorer tree, and Shiki code syntax highlighting.

TypeScriptNext.jsReact 19Tailwind CSSShiki

Contributors

1