diff --git a/src/app/AppRouter.tsx b/src/app/AppRouter.tsx index 712135d..9fb9eff 100644 --- a/src/app/AppRouter.tsx +++ b/src/app/AppRouter.tsx @@ -6,13 +6,14 @@ import Footer from '../components/layout/Footer'; import HomePage from '../pages/HomePage'; import ImprintPage from '../pages/ImprintPage'; import PrivacyPolicy from '../pages/PrivacyPolicy'; +import type { Theme } from '../data/types'; function AppRouter() { - const [theme, setTheme] = useState<'light' | 'dark'>('light'); + const [theme, setTheme] = useState('light'); useEffect(() => { // Check for saved theme preference or default to light mode - const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null; + const savedTheme = localStorage.getItem('theme') as Theme | null; if (savedTheme) { setTheme(savedTheme); } diff --git a/src/components/LanguageToggle.tsx b/src/components/LanguageToggle.tsx index 9114e31..5e8bda6 100644 --- a/src/components/LanguageToggle.tsx +++ b/src/components/LanguageToggle.tsx @@ -1,10 +1,11 @@ import { useLanguage } from '../contexts/LanguageContext'; +import type { Language } from '../data/types'; import '../scss/language-toggle.scss'; export default function LanguageToggle() { const { language, setLanguage } = useLanguage(); - const handleLanguageChange = (newLang: 'en' | 'de') => { + const handleLanguageChange = (newLang: Language) => { if (newLang !== language) { setLanguage(newLang); // Announce language change to screen readers diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index cd1a0d8..96c1285 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { useLanguage } from '../contexts/LanguageContext'; +import type { Theme } from '../data/types'; type Props = Readonly<{ - theme: 'light' | 'dark'; - setTheme: React.Dispatch>; + theme: Theme; + setTheme: React.Dispatch>; }>; export default function ThemeToggle({ diff --git a/src/components/layout/MobileMenu.tsx b/src/components/layout/MobileMenu.tsx index e1f798c..88f5bf1 100644 --- a/src/components/layout/MobileMenu.tsx +++ b/src/components/layout/MobileMenu.tsx @@ -1,12 +1,14 @@ import { useEffect, useRef } from 'react'; +import type { MenuItem } from '../../data/types'; type Props = { - readonly menuItems: string[]; + readonly menuItems: MenuItem[]; readonly isOpen: boolean; readonly onItemClick: (section: string) => void; + readonly onClose: () => void; }; -export default function MobileMenu({ menuItems, isOpen, onItemClick }: Props) { +export default function MobileMenu({ menuItems, isOpen, onItemClick, onClose }: Props) { const firstButtonRef = useRef(null); useEffect(() => { @@ -19,17 +21,29 @@ export default function MobileMenu({ menuItems, isOpen, onItemClick }: Props) { return ( ); } diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index 3467635..37fcd2a 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import ThemeToggle from '../ThemeToggle'; import LanguageToggle from '../LanguageToggle'; @@ -6,10 +6,11 @@ import MobileMenu from './MobileMenu'; import { personalConfig } from '../../config/personal'; import { useLanguage } from '../../contexts/LanguageContext'; import { scrollToSection } from '../../utils/scrollUtils'; +import type { Theme } from '../../data/types'; type Props = { - theme: 'light' | 'dark'; - setTheme: React.Dispatch>; + theme: Theme; + setTheme: React.Dispatch>; }; export default function Navbar({ @@ -17,12 +18,39 @@ export default function Navbar({ setTheme }: Readonly) { const [menuOpen, setMenuOpen] = useState(false); + const [activeSection, setActiveSection] = useState(''); const location = useLocation(); const navigate = useNavigate(); const { texts } = useLanguage(); const menuItems = texts.navigation.menuItems; + // Track active section based on scroll position + useEffect(() => { + if (location.pathname !== '/') { + setActiveSection(''); + return; + } + + const handleScroll = () => { + const sections = menuItems.map(item => item.section.toLowerCase()); + const scrollPosition = window.scrollY + 100; // Offset for navbar height + + for (const section of sections.reverse()) { + const element = document.getElementById(section); + if (element && element.offsetTop <= scrollPosition) { + setActiveSection(section); + return; + } + } + setActiveSection(''); + }; + + handleScroll(); // Initial check + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, [location.pathname, menuItems]); + function handleNavigation(section: string): void { const sectionId = section.toLowerCase(); scrollToSection(sectionId, location.pathname, navigate); @@ -37,11 +65,11 @@ export default function Navbar({
{menuItems.map((item) => ( ))} @@ -62,6 +90,7 @@ export default function Navbar({ menuItems={menuItems} isOpen={menuOpen} onItemClick={handleNavigation} + onClose={() => setMenuOpen(false)} />
diff --git a/src/config/locales/de/contact.ts b/src/config/locales/de/contact.ts index 8c811fb..da05e13 100644 --- a/src/config/locales/de/contact.ts +++ b/src/config/locales/de/contact.ts @@ -1,7 +1,7 @@ export const contact = { title: 'Kontakt aufnehmen', - subtitle: 'Lassen Sie uns Ihr Projekt besprechen und Ihre Ideen zum Leben erwecken', - connectTitle: 'Lassen Sie uns verbinden', + subtitle: 'Besprechen Sie Ihr Projekt mit mir und wir erwecken Ihre Ideen zum Leben.', + connectTitle: 'Verbindung schaffen', connectDescription: 'Ich bin immer an neuen Möglichkeiten und spannenden Projekten interessiert. Ob Sie eine Frage haben oder einfach nur Hallo sagen möchten, zögern Sie nicht, mich zu kontaktieren!', buttonText: 'Per E-Mail kontaktieren', diff --git a/src/config/locales/de/navigation.ts b/src/config/locales/de/navigation.ts index ba1ab75..81ef7b3 100644 --- a/src/config/locales/de/navigation.ts +++ b/src/config/locales/de/navigation.ts @@ -1,11 +1,11 @@ export const navigation = { menuItems: [ - 'Über mich', - 'Leistungen', - 'Fähigkeiten', - 'Zertifikate', - 'Projekte', - 'Kontakt', + { section: 'About', label: 'Über mich' }, + { section: 'Services', label: 'Leistungen' }, + { section: 'Skills', label: 'Fähigkeiten' }, + { section: 'Certifications', label: 'Zertifikate' }, + { section: 'Projects', label: 'Projekte' }, + { section: 'Contact', label: 'Kontakt' }, ], mobileMenuAriaLabel: 'Menü öffnen', }; diff --git a/src/config/locales/en/index.ts b/src/config/locales/en/index.ts index a18654c..854c75c 100644 --- a/src/config/locales/en/index.ts +++ b/src/config/locales/en/index.ts @@ -11,12 +11,13 @@ import { contact } from './contact'; import { imprint } from './imprint'; import { privacyPolicy } from './privacy-policy'; import { accessibility } from './accessibility'; +import type { MenuItem } from '../../../data/types'; // TypeScript interfaces for type safety export interface TextConfig { // Navigation navigation: { - menuItems: string[]; + menuItems: MenuItem[]; mobileMenuAriaLabel: string; }; @@ -153,7 +154,7 @@ export interface TextConfig { dataProtectionOfficerText: string; rightsTitle: string; rightsIntro: string; - rights: Array<{ title: string; text: string }>; + rights: Array<{ title: string; text: string; }>; revocationTitle: string; revocationText: string; objectionTitle: string; diff --git a/src/config/locales/en/navigation.ts b/src/config/locales/en/navigation.ts index ff575ca..48b03e8 100644 --- a/src/config/locales/en/navigation.ts +++ b/src/config/locales/en/navigation.ts @@ -1,11 +1,11 @@ export const navigation = { menuItems: [ - 'About', - 'Services', - 'Skills', - 'Certifications', - 'Projects', - 'Contact', + { section: 'About', label: 'About' }, + { section: 'Services', label: 'Services' }, + { section: 'Skills', label: 'Skills' }, + { section: 'Certifications', label: 'Certifications' }, + { section: 'Projects', label: 'Projects' }, + { section: 'Contact', label: 'Contact' }, ], mobileMenuAriaLabel: 'Menü öffnen', }; diff --git a/src/contexts/LanguageContext.tsx b/src/contexts/LanguageContext.tsx index 464ce91..6d7208e 100644 --- a/src/contexts/LanguageContext.tsx +++ b/src/contexts/LanguageContext.tsx @@ -1,8 +1,7 @@ import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; import { en, type TextConfig } from '../config/locales/en'; import { de } from '../config/locales/de'; - -type Language = 'en' | 'de'; +import type { Language } from '../data/types'; interface LanguageContextType { language: Language; @@ -30,7 +29,7 @@ function getSavedLanguage(): Language | null { return null; } -export function LanguageProvider({ children }: { children: ReactNode }) { +export function LanguageProvider({ children }: { children: ReactNode; }) { const [language, setLanguageState] = useState(() => { // First check localStorage, then browser language, then default to English return getSavedLanguage() || getBrowserLanguage(); diff --git a/src/data/types.ts b/src/data/types.ts new file mode 100644 index 0000000..e8622b9 --- /dev/null +++ b/src/data/types.ts @@ -0,0 +1,15 @@ +/** + * Common types used across the application + */ + +// Navigation types +export interface MenuItem { + section: string; + label: string; +} + +// Language types +export type Language = 'en' | 'de'; + +// Theme types +export type Theme = 'light' | 'dark'; diff --git a/src/scss/layout/topbar.scss b/src/scss/layout/topbar.scss index a7a7c98..6bd05db 100644 --- a/src/scss/layout/topbar.scss +++ b/src/scss/layout/topbar.scss @@ -7,6 +7,10 @@ background: var(--color-background); color: var(--color-text); height: 65px; + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); &__name { font-weight: bold; @@ -25,12 +29,19 @@ @extend %hover-lift; padding: 0.6rem 1.2rem; border-radius: 8px; - transition: box-shadow 0.2s ease, transform 0.2s ease; + transition: + box-shadow 0.2s ease, + transform 0.2s ease; &:active { box-shadow: 0 2px 4px var(--box-shadow-active); transform: translateY(0); } + + &--active { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + background: var(--box-shadow-hover, rgba(0, 0, 0, 0.05)); + } } } @@ -74,23 +85,55 @@ /* Mobile Menu Dropdown */ .navbar__mobile-menu { - @include globals.flex-center(); position: absolute; + top: 65px; right: 0; background: var(--color-background); border: 1px solid var(--color-primary); border-radius: 8px; - padding: 0.5em; - gap: 0.5em; + padding: 0; box-shadow: 0 4px 12px var(--box-shadow-hover); z-index: 10; + min-width: 200px; - button { + &__header { + display: flex; + justify-content: flex-end; + padding: 0.5em; + border-bottom: 1px solid var(--color-border, rgba(0, 0, 0, 0.1)); + } + + &__close { @extend %button-reset; - padding: 0.5em 1em; - text-align: right; + font-size: 1.5rem; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; border-radius: 4px; transition: background 0.2s; + color: var(--color-text); + + &:hover { + background: var(--box-shadow-active); + } + } + + &__items { + display: flex; + flex-direction: column; + gap: 0; + padding: 0.5em; + } + + &__item { + @extend %button-reset; + padding: 0.75em 1em; + text-align: left; + border-radius: 4px; + transition: background 0.2s; + width: 100%; &:hover { background: var(--box-shadow-active); diff --git a/src/scss/sections/projects-section.scss b/src/scss/sections/projects-section.scss index f4c86b0..3bb4751 100644 --- a/src/scss/sections/projects-section.scss +++ b/src/scss/sections/projects-section.scss @@ -446,6 +446,16 @@ } } +// Responsive layout for screens smaller than 1050px +@media (max-width: 1050px) { + .projects-section { + &__actions { + flex-direction: column; + gap: 0.5rem; + } + } +} + // Dark theme adjustments for help section: @media (prefers-color-scheme: dark) { .projects-section {