📱mobile responsiveness for navbar Menu

This commit is contained in:
RJ
2025-12-05 15:51:16 +02:00
parent bba507a7e8
commit 919e638144
8 changed files with 355 additions and 27 deletions

View File

@@ -2,7 +2,7 @@ import { Link } from '@/src/i18n/navigation'
import Image from 'next/image' import Image from 'next/image'
import { getAllPosts } from '@/lib/markdown' import { getAllPosts } from '@/lib/markdown'
import { formatDate } from '@/lib/utils' import { formatDate } from '@/lib/utils'
import { ThemeToggle } from '@/components/theme-toggle' import { HeroHeader } from '@/components/layout/hero-header'
import { setRequestLocale, getTranslations } from 'next-intl/server' import { setRequestLocale, getTranslations } from 'next-intl/server'
type Props = { type Props = {
@@ -29,29 +29,11 @@ export default async function HomePage({ params }: Props) {
<div className="relative z-10 max-w-5xl mx-auto px-6 w-full"> <div className="relative z-10 max-w-5xl mx-auto px-6 w-full">
<div className="border-4 border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-900/80 p-8 md:p-12 transition-colors duration-300"> <div className="border-4 border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-900/80 p-8 md:p-12 transition-colors duration-300">
{/* Logo */} {/* Logo */}
<div className="mb-8 flex items-center justify-between border-b-2 border-slate-300 dark:border-slate-800 pb-4"> <HeroHeader
<div className="flex items-center gap-3"> terminalVersion={t('terminalVersion')}
<Image src="/logo.png" alt="Logo" width={32} height={32} className="opacity-80" /> blogLabel={tNav('blog')}
<span className="font-mono text-xs text-slate-500 uppercase tracking-widest"> aboutLabel={tNav('about')}
{t('terminalVersion')} />
</span>
</div>
<div className="flex gap-4 items-center">
<Link
href="/blog"
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400"
>
[{tNav('blog')}]
</Link>
<Link
href="/about"
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400"
>
[{tNav('about')}]
</Link>
<ThemeToggle />
</div>
</div>
<div className="border-l-4 border-cyan-700 dark:border-cyan-900 pl-6 mb-8"> <div className="border-l-4 border-cyan-700 dark:border-cyan-900 pl-6 mb-8">
<p className="font-mono text-xs text-slate-500 dark:text-slate-500 uppercase tracking-widest mb-2"> <p className="font-mono text-xs text-slate-500 dark:text-slate-500 uppercase tracking-widest mb-2">

View File

@@ -449,3 +449,83 @@
margin-bottom: 3rem; margin-bottom: 3rem;
} }
} }
/* === MOBILE RESPONSIVE UTILITIES === */
@media (max-width: 767px) {
.show-mobile-only {
display: block;
}
.hide-mobile {
display: none !important;
}
}
@media (min-width: 768px) {
.show-mobile-only {
display: none !important;
}
.hide-mobile {
display: block;
}
}
/* === BUTTON GLITCH EFFECT === */
@layer utilities {
.glitch-btn-cyber {
--glitch-shimmy: 5;
--glitch-clip-1: polygon(0 2%, 100% 2%, 100% 95%, 95% 95%, 95% 90%, 85% 90%, 85% 95%, 8% 95%, 0 70%);
--glitch-clip-2: polygon(0 78%, 100% 78%, 100% 100%, 95% 100%, 95% 90%, 85% 90%, 85% 100%, 8% 100%, 0 78%);
--glitch-clip-3: polygon(0 44%, 100% 44%, 100% 54%, 95% 54%, 95% 54%, 85% 54%, 85% 54%, 8% 54%, 0 54%);
--glitch-clip-4: polygon(0 0, 100% 0, 100% 0, 95% 0, 95% 0, 85% 0, 85% 0, 8% 0, 0 0);
--glitch-clip-5: polygon(0 40%, 100% 40%, 100% 85%, 95% 85%, 95% 85%, 85% 85%, 85% 85%, 8% 85%, 0 70%);
--glitch-clip-6: polygon(0 63%, 100% 63%, 100% 80%, 95% 80%, 95% 80%, 85% 80%, 85% 80%, 8% 80%, 0 70%);
}
.glitch-overlay {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
pointer-events: none;
color: var(--neon-cyan);
z-index: 10;
}
.glitch-btn-cyber:is(:hover, :focus-visible) .glitch-overlay {
display: flex;
animation: glitch-btn-animate 2s infinite;
}
@keyframes glitch-btn-animate {
0% { clip-path: var(--glitch-clip-1); }
2%, 8% { clip-path: var(--glitch-clip-2); transform: translate(calc(var(--glitch-shimmy) * -1%), 0); }
6% { clip-path: var(--glitch-clip-2); transform: translate(calc(var(--glitch-shimmy) * 1%), 0); }
9% { clip-path: var(--glitch-clip-2); transform: translate(0, 0); }
10% { clip-path: var(--glitch-clip-3); transform: translate(calc(var(--glitch-shimmy) * 1%), 0); }
13% { clip-path: var(--glitch-clip-3); transform: translate(0, 0); }
14%, 21% { clip-path: var(--glitch-clip-4); transform: translate(calc(var(--glitch-shimmy) * 1%), 0); }
25%, 30% { clip-path: var(--glitch-clip-5); transform: translate(calc(var(--glitch-shimmy) * -1%), 0); }
35%, 45% { clip-path: var(--glitch-clip-6); transform: translate(calc(var(--glitch-shimmy) * -1%), 0); }
40% { clip-path: var(--glitch-clip-6); transform: translate(calc(var(--glitch-shimmy) * 1%), 0); }
50% { clip-path: var(--glitch-clip-6); transform: translate(0, 0); }
55% { clip-path: var(--glitch-clip-3); transform: translate(calc(var(--glitch-shimmy) * 1%), 0); }
60% { clip-path: var(--glitch-clip-3); transform: translate(0, 0); }
61%, 100% { clip-path: var(--glitch-clip-4); }
}
.glitch-btn-subtle {
--glitch-shimmy: 2;
}
.glitch-overlay-pink { color: var(--neon-pink); }
.glitch-overlay-purple { color: var(--neon-purple); }
.glitch-overlay-magenta { color: var(--neon-magenta); }
@media (prefers-reduced-motion: reduce) {
.glitch-btn-cyber:is(:hover, :focus-visible) .glitch-overlay {
animation: none;
display: none;
}
}
}

View File

@@ -5,11 +5,13 @@ import { useTranslations } from 'next-intl'
import { Link } from '@/i18n/navigation' import { Link } from '@/i18n/navigation'
import { ThemeToggle } from '@/components/theme-toggle' import { ThemeToggle } from '@/components/theme-toggle'
import LanguageSwitcher from '@/components/layout/LanguageSwitcher' import LanguageSwitcher from '@/components/layout/LanguageSwitcher'
import { GlitchButton } from '@/components/effects/glitch-button'
export function Navbar() { export function Navbar() {
const t = useTranslations('Navigation') const t = useTranslations('Navigation')
const [isVisible, setIsVisible] = useState(true) const [isVisible, setIsVisible] = useState(true)
const [lastScrollY, setLastScrollY] = useState(0) const [lastScrollY, setLastScrollY] = useState(0)
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
@@ -19,6 +21,7 @@ export function Navbar() {
setIsVisible(true) setIsVisible(true)
} else if (currentScrollY > lastScrollY) { } else if (currentScrollY > lastScrollY) {
setIsVisible(false) setIsVisible(false)
setIsMobileMenuOpen(false)
} else { } else {
setIsVisible(true) setIsVisible(true)
} }
@@ -44,11 +47,12 @@ export function Navbar() {
> >
&lt; {t('home')} &lt; {t('home')}
</Link> </Link>
<span className="font-mono text-sm text-zinc-100 dark:text-zinc-300 uppercase tracking-wider"> <span className="font-mono text-sm text-zinc-100 dark:text-zinc-300 uppercase tracking-wider hidden md:block">
// <span style={{ color: 'var(--neon-pink)' }}>{t('blog')}</span> ARCHIVE // <span style={{ color: 'var(--neon-pink)' }}>{t('blog')}</span> ARCHIVE
</span> </span>
</div> </div>
<div className="flex items-center gap-6">
<div className="hidden md:flex items-center gap-6">
<Link <Link
href="/about" href="/about"
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer" className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer"
@@ -64,7 +68,45 @@ export function Navbar() {
<ThemeToggle /> <ThemeToggle />
<LanguageSwitcher /> <LanguageSwitcher />
</div> </div>
<div className="md:hidden flex items-center gap-4">
<GlitchButton
variant="subtle"
glitchColor="cyan"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="font-mono text-sm uppercase tracking-wider px-4 py-2 border-4 border-slate-700 bg-slate-800 dark:bg-zinc-900 text-zinc-100 dark:text-zinc-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500"
aria-label="Toggle mobile menu"
aria-expanded={isMobileMenuOpen}
>
// {isMobileMenuOpen ? 'CLOSE' : 'MENU'}
</GlitchButton>
</div>
</div> </div>
{isMobileMenuOpen && (
<div className="md:hidden mt-4 pt-4 border-t-4 border-slate-700">
<div className="flex flex-col gap-4">
<Link
href="/about"
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer px-4 py-2 border-2 border-slate-700"
onClick={() => setIsMobileMenuOpen(false)}
>
[{t('about')}]
</Link>
<Link
href="/blog"
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer px-4 py-2 border-2 border-slate-700"
onClick={() => setIsMobileMenuOpen(false)}
>
[{t('blog')}]
</Link>
<div className="flex items-center gap-4 px-4 py-2">
<ThemeToggle />
<LanguageSwitcher />
</div>
</div>
</div>
)}
</div> </div>
</nav> </nav>
) )

View File

@@ -0,0 +1,71 @@
'use client'
import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl'
import { Link } from '@/i18n/navigation'
import { ThemeToggle } from '@/components/theme-toggle'
import LanguageSwitcher from '@/components/layout/LanguageSwitcher'
export function Navbar() {
const t = useTranslations('Navigation')
const [isVisible, setIsVisible] = useState(true)
const [lastScrollY, setLastScrollY] = useState(0)
useEffect(() => {
const handleScroll = () => {
const currentScrollY = window.scrollY
if (currentScrollY < 10) {
setIsVisible(true)
} else if (currentScrollY > lastScrollY) {
setIsVisible(false)
} else {
setIsVisible(true)
}
setLastScrollY(currentScrollY)
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [lastScrollY])
return (
<nav
className={`border-b-4 border-slate-700 bg-slate-900 dark:bg-zinc-950 sticky top-0 z-50 ${isVisible ? 'navbar-visible' : 'navbar-hidden'}`}
>
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-8">
<Link
href="/"
className="font-mono text-sm uppercase tracking-wider transition-colors cursor-pointer"
style={{ color: 'var(--neon-cyan)' }}
>
&lt; {t('home')}
</Link>
<span className="font-mono text-sm text-zinc-100 dark:text-zinc-300 uppercase tracking-wider">
// <span style={{ color: 'var(--neon-pink)' }}>{t('blog')}</span> ARCHIVE
</span>
</div>
<div className="flex items-center gap-6">
<Link
href="/about"
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer"
>
[{t('about')}]
</Link>
<Link
href="/blog"
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer"
>
[{t('blog')}]
</Link>
<ThemeToggle />
<LanguageSwitcher />
</div>
</div>
</div>
</nav>
)
}

View File

@@ -0,0 +1,54 @@
'use client'
import React from 'react'
import { cn } from '@/lib/utils'
interface GlitchButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode
variant?: 'default' | 'subtle'
glitchColor?: 'cyan' | 'pink' | 'purple' | 'magenta'
disabled?: boolean
}
export function GlitchButton({
children,
variant = 'default',
glitchColor = 'cyan',
disabled = false,
className,
...props
}: GlitchButtonProps) {
const glitchClasses = !disabled
? cn(
'glitch-btn-cyber',
variant === 'subtle' && 'glitch-btn-subtle',
'relative'
)
: ''
const overlayColorClass = {
cyan: '',
pink: 'glitch-overlay-pink',
purple: 'glitch-overlay-purple',
magenta: 'glitch-overlay-magenta',
}[glitchColor]
return (
<button
className={cn(glitchClasses, className)}
disabled={disabled}
{...props}
>
{children}
{!disabled && (
<div
className={cn('glitch-overlay', overlayColorClass)}
aria-hidden="true"
>
{children}
</div>
)}
</button>
)
}

View File

@@ -0,0 +1,95 @@
'use client'
import { useState, useEffect } from 'react'
import { Link } from '@/i18n/navigation'
import Image from 'next/image'
import { ThemeToggle } from '@/components/theme-toggle'
import { GlitchButton } from '@/components/effects/glitch-button'
interface HeroHeaderProps {
terminalVersion: string
blogLabel: string
aboutLabel: string
}
export function HeroHeader({ terminalVersion, blogLabel, aboutLabel }: HeroHeaderProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768)
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
return (
<div className="mb-8 border-b-2 border-slate-300 dark:border-slate-800 pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Image src="/logo.png" alt="Logo" width={32} height={32} className="opacity-80" />
<span className="font-mono text-xs text-slate-500 uppercase tracking-widest">
{terminalVersion}
</span>
</div>
{!isMobile && (
<div className="flex gap-4 items-center">
<Link
href="/blog"
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400"
>
[{blogLabel}]
</Link>
<Link
href="/about"
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400"
>
[{aboutLabel}]
</Link>
<ThemeToggle />
</div>
)}
{isMobile && (
<div>
<GlitchButton
variant="subtle"
glitchColor="cyan"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="font-mono text-xs uppercase tracking-wider px-3 py-2 border-2 border-slate-400 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300"
aria-label="Toggle menu"
aria-expanded={isMobileMenuOpen}
>
// {isMobileMenuOpen ? 'X' : 'MENU'}
</GlitchButton>
</div>
)}
</div>
{isMobileMenuOpen && isMobile && (
<div className="mt-4 pt-4 border-t-2 border-slate-300 dark:border-slate-800">
<div className="flex flex-col gap-3">
<Link
href="/blog"
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400 px-3 py-2 border-2 border-slate-300 dark:border-slate-700"
onClick={() => setIsMobileMenuOpen(false)}
>
[{blogLabel}]
</Link>
<Link
href="/about"
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400 px-3 py-2 border-2 border-slate-300 dark:border-slate-700"
onClick={() => setIsMobileMenuOpen(false)}
>
[{aboutLabel}]
</Link>
<div className="px-3 py-2">
<ThemeToggle />
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -79,3 +79,7 @@ export function generateSlug(title: string): string {
.replace(/[^a-z0-9]+/g, '-') .replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') .replace(/^-+|-+$/g, '')
} }
export function cn(...inputs: (string | undefined | null | false)[]): string {
return inputs.filter(Boolean).join(" ")
}

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.