diff --git a/app/[locale]/@breadcrumbs/about/page.tsx b/app/[locale]/@breadcrumbs/about/page.tsx index 7584620..8670f2e 100644 --- a/app/[locale]/@breadcrumbs/about/page.tsx +++ b/app/[locale]/@breadcrumbs/about/page.tsx @@ -1,11 +1,16 @@ +'use client' + import { Breadcrumbs } from '@/components/layout/Breadcrumbs' +import { useTranslations } from 'next-intl' export default function AboutBreadcrumb() { + const t = useTranslations('Breadcrumbs') + return ( }) { + const t = await getTranslations('Breadcrumbs') const { slug } = await params const slugPath = slug.join('/') const post = await getPostBySlug(slugPath) const items: BreadcrumbItem[] = [ { - label: 'Blog', + label: t('blog'), href: '/blog', }, ] @@ -36,8 +28,9 @@ export default async function BlogPostBreadcrumb({ if (slug.length > 1) { for (let i = 0; i < slug.length - 1; i++) { const segmentPath = slug.slice(0, i + 1).join('/') + const dirName = slug[i] items.push({ - label: formatDirectoryName(slug[i]), + label: t(dirName) || dirName.charAt(0).toUpperCase() + dirName.slice(1), href: `/blog/${segmentPath}`, }) } diff --git a/app/[locale]/@breadcrumbs/blog/page.tsx b/app/[locale]/@breadcrumbs/blog/page.tsx index 44e92ed..c2ce44d 100644 --- a/app/[locale]/@breadcrumbs/blog/page.tsx +++ b/app/[locale]/@breadcrumbs/blog/page.tsx @@ -1,11 +1,16 @@ +'use client' + import { Breadcrumbs } from '@/components/layout/Breadcrumbs' +import { useTranslations } from 'next-intl' export default function BlogBreadcrumb() { + const t = useTranslations('Breadcrumbs') + return ( }) { + const t = await getTranslations('Breadcrumbs') const { tag } = await params const tagName = tag .split('-') @@ -11,7 +13,7 @@ export default async function TagBreadcrumb({ params }: { params: Promise<{ tag:
{/* Logo */} -
-
- Logo - - {t('terminalVersion')} - -
-
- - [{tNav('blog')}] - - - [{tNav('about')}] - - -
-
+

diff --git a/app/globals.css b/app/globals.css index 7d82f03..940e4e1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -449,3 +449,184 @@ 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/effects/glitch-button.tsx b/components/effects/glitch-button.tsx new file mode 100644 index 0000000..a33343a --- /dev/null +++ b/components/effects/glitch-button.tsx @@ -0,0 +1,43 @@ +'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..6ab11fc --- /dev/null +++ b/components/layout/hero-header.tsx @@ -0,0 +1,99 @@ +'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' +import LanguageSwitcher from './LanguageSwitcher' +import { useLocale, useTranslations } from 'next-intl' + +export function HeroHeader() { + const locale = useLocale() + const t = useTranslations('Home') + const tNav = useTranslations('Navigation') + + const terminalVersion = t('terminalVersion') + const blogLabel = tNav('blog') + const aboutLabel = tNav('about') + + 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..08bce76 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/messages/en.json b/messages/en.json index a585bd5..aa35544 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3,21 +3,21 @@ "siteTitle": "Personal Blog", "siteDescription": "Thoughts on technology and development" }, - "Navigation": { "home": "Home", "blog": "Blog", "tags": "Tags", "about": "About" }, - "Breadcrumbs": { "home": "Home", "blog": "Blog", "tags": "Tags", - "about": "About" + "about": "About", + "tech": "Technology", + "design": "Design", + "tutorial": "Tutorials" }, - "Home": { "terminalVersion": "TERMINAL:// V2.0", "documentLevel": "DOCUMENT LEVEL-1 //", @@ -32,7 +32,6 @@ "seePostsButton": "[SEE POSTS] >>", "seeAllTagsButton": "[SEE ALL TAGS] >>" }, - "BlogListing": { "title": "Blog", "subtitle": "Latest articles and thoughts", @@ -48,7 +47,6 @@ "prev": "< PREV", "next": "NEXT >" }, - "BlogPost": { "readMore": "Read more", "readingTime": "{minutes} min read", @@ -58,7 +56,6 @@ "relatedPosts": "Related Posts", "sharePost": "Share this post" }, - "Tags": { "title": "Tags", "subtitle": "Browse by topic", @@ -67,7 +64,6 @@ "relatedTags": "Related tags", "quickNav": "Quick navigation" }, - "About": { "title": "About", "subtitle": "Learn more about me", @@ -119,13 +115,11 @@ "techStackSelfHostingText": "Home lab, privacy-focused services, full control, Git server", "contactTitle": "> CONTACT" }, - "NotFound": { "title": "Page Not Found", "description": "The page you're looking for doesn't exist", "goHome": "Go to homepage" }, - "LanguageSwitcher": { "switchLanguage": "Switch language", "currentLanguage": "Current language" diff --git a/messages/ro.json b/messages/ro.json index 5f7f57a..55ff6b4 100644 --- a/messages/ro.json +++ b/messages/ro.json @@ -13,15 +13,18 @@ "home": "Acasă", "blog": "Blog", "tags": "Etichete", - "about": "Despre" + "about": "Despre", + "tech": "Tehnologie", + "design": "Design", + "tutorial": "Tutoriale" }, "Home": { "terminalVersion": "TERMINAL:// V2.0", "documentLevel": "DOCUMENT LEVEL-1 //", "heroTitle": "BUILD. WRITE. SHARE.", "heroSubtitle": "> Explore ideas", - "checkPostsButton": "[CHECK POSTS]", - "aboutMeButton": "[ABOUT ME]", + "checkPostsButton": "[POSTĂRI]", + "aboutMeButton": "[DESPRE]", "recentEntriesLabel": "ARCHIVE ACCESS // RECENT ENTRIES", "recentEntriesTitle": "> RECENT ENTRIES", "fileLabel": "FILE#{number} // {category}", 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.