diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 72e2f9d..d71ecd3 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -2,7 +2,7 @@ import { Link } from '@/src/i18n/navigation' import Image from 'next/image' import { getAllPosts } from '@/lib/markdown' 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' type Props = { @@ -29,29 +29,11 @@ export default async function HomePage({ params }: Props) {
{/* Logo */} -
-
- Logo - - {t('terminalVersion')} - -
-
- - [{tNav('blog')}] - - - [{tNav('about')}] - - -
-
+

diff --git a/app/globals.css b/app/globals.css index 7d82f03..3340151 100644 --- a/app/globals.css +++ b/app/globals.css @@ -449,3 +449,83 @@ 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; + } + } +} diff --git a/components/blog/navbar.tsx b/components/blog/navbar.tsx index d226c3d..476a7c0 100644 --- a/components/blog/navbar.tsx +++ b/components/blog/navbar.tsx @@ -5,11 +5,13 @@ import { useTranslations } from 'next-intl' import { Link } from '@/i18n/navigation' import { ThemeToggle } from '@/components/theme-toggle' import LanguageSwitcher from '@/components/layout/LanguageSwitcher' +import { GlitchButton } from '@/components/effects/glitch-button' export function Navbar() { const t = useTranslations('Navigation') const [isVisible, setIsVisible] = useState(true) const [lastScrollY, setLastScrollY] = useState(0) + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) useEffect(() => { const handleScroll = () => { @@ -19,6 +21,7 @@ export function Navbar() { setIsVisible(true) } else if (currentScrollY > lastScrollY) { setIsVisible(false) + setIsMobileMenuOpen(false) } else { setIsVisible(true) } @@ -44,11 +47,12 @@ export function Navbar() { > < {t('home')} - + // {t('blog')} ARCHIVE

-
+ +
+ +
+ 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'} + +
+ + {isMobileMenuOpen && ( +
+
+ setIsMobileMenuOpen(false)} + > + [{t('about')}] + + setIsMobileMenuOpen(false)} + > + [{t('blog')}] + +
+ + +
+
+
+ )}
) diff --git a/components/blog/navbar.tsx.bak b/components/blog/navbar.tsx.bak new file mode 100644 index 0000000..d226c3d --- /dev/null +++ b/components/blog/navbar.tsx.bak @@ -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 ( + + ) +} diff --git a/components/effects/glitch-button.tsx b/components/effects/glitch-button.tsx new file mode 100644 index 0000000..ebdbffc --- /dev/null +++ b/components/effects/glitch-button.tsx @@ -0,0 +1,54 @@ +'use client' + +import React from 'react' +import { cn } from '@/lib/utils' + +interface GlitchButtonProps extends React.ButtonHTMLAttributes { + 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 ( + + ) +} diff --git a/components/layout/hero-header.tsx b/components/layout/hero-header.tsx new file mode 100644 index 0000000..07ef4f1 --- /dev/null +++ b/components/layout/hero-header.tsx @@ -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 ( +
+
+
+ Logo + + {terminalVersion} + +
+ + {!isMobile && ( +
+ + [{blogLabel}] + + + [{aboutLabel}] + + +
+ )} + + {isMobile && ( +
+ 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'} + +
+ )} +
+ + {isMobileMenuOpen && isMobile && ( +
+
+ setIsMobileMenuOpen(false)} + > + [{blogLabel}] + + setIsMobileMenuOpen(false)} + > + [{aboutLabel}] + +
+ +
+
+
+ )} +
+ ) +} diff --git a/lib/utils.ts b/lib/utils.ts index e2a1692..218e52e 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -79,3 +79,7 @@ export function generateSlug(title: string): string { .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') } + +export function cn(...inputs: (string | undefined | null | false)[]): string { + return inputs.filter(Boolean).join(" ") +} diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.