diff --git a/public/assets/backgrounds/mobile-menu-background.webp b/public/assets/backgrounds/mobile-menu-background.webp
new file mode 100644
index 0000000..5188c51
Binary files /dev/null and b/public/assets/backgrounds/mobile-menu-background.webp differ
diff --git a/public/assets/decorations/language-hover-de.svg b/public/assets/decorations/language-hover-de.svg
new file mode 100644
index 0000000..4d52131
--- /dev/null
+++ b/public/assets/decorations/language-hover-de.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/assets/decorations/language-hover-en.svg b/public/assets/decorations/language-hover-en.svg
new file mode 100644
index 0000000..263fce7
--- /dev/null
+++ b/public/assets/decorations/language-hover-en.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/assets/decorations/navigation/nav-hover-about.svg b/public/assets/decorations/navigation/nav-hover-about.svg
new file mode 100644
index 0000000..9012c45
--- /dev/null
+++ b/public/assets/decorations/navigation/nav-hover-about.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/assets/decorations/navigation/nav-hover-contact.svg b/public/assets/decorations/navigation/nav-hover-contact.svg
new file mode 100644
index 0000000..80f114b
--- /dev/null
+++ b/public/assets/decorations/navigation/nav-hover-contact.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/assets/decorations/navigation/nav-hover-projects.svg b/public/assets/decorations/navigation/nav-hover-projects.svg
new file mode 100644
index 0000000..feb9cd4
--- /dev/null
+++ b/public/assets/decorations/navigation/nav-hover-projects.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/assets/decorations/navigation/nav-hover-skills.svg b/public/assets/decorations/navigation/nav-hover-skills.svg
new file mode 100644
index 0000000..8732dc9
--- /dev/null
+++ b/public/assets/decorations/navigation/nav-hover-skills.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/assets/icons/navigation/close-active.svg b/public/assets/icons/navigation/close-active.svg
new file mode 100644
index 0000000..c144722
--- /dev/null
+++ b/public/assets/icons/navigation/close-active.svg
@@ -0,0 +1,15 @@
+
diff --git a/public/assets/icons/navigation/menu-active.svg b/public/assets/icons/navigation/menu-active.svg
new file mode 100644
index 0000000..e30f3f6
--- /dev/null
+++ b/public/assets/icons/navigation/menu-active.svg
@@ -0,0 +1,15 @@
+
diff --git a/public/assets/icons/navigation/menu-close.svg b/public/assets/icons/navigation/menu-close.svg
new file mode 100644
index 0000000..dc5c87d
--- /dev/null
+++ b/public/assets/icons/navigation/menu-close.svg
@@ -0,0 +1,8 @@
+
diff --git a/public/assets/icons/navigation/menu-open.svg b/public/assets/icons/navigation/menu-open.svg
new file mode 100644
index 0000000..f3cb6d0
--- /dev/null
+++ b/public/assets/icons/navigation/menu-open.svg
@@ -0,0 +1,8 @@
+
diff --git a/public/assets/icons/social/email.svg b/public/assets/icons/social/email.svg
new file mode 100644
index 0000000..3d3ace8
--- /dev/null
+++ b/public/assets/icons/social/email.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/assets/icons/social/github.svg b/public/assets/icons/social/github.svg
new file mode 100644
index 0000000..2e80b2f
--- /dev/null
+++ b/public/assets/icons/social/github.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/assets/icons/social/linkedin.svg b/public/assets/icons/social/linkedin.svg
new file mode 100644
index 0000000..0f48519
--- /dev/null
+++ b/public/assets/icons/social/linkedin.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/logo.svg b/public/logo.svg
new file mode 100644
index 0000000..dead462
--- /dev/null
+++ b/public/logo.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index cb1270e..166836f 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -1,11 +1,16 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
-import { provideRouter } from '@angular/router';
+import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
- provideRouter(routes)
- ]
+ provideRouter(
+ routes,
+ withInMemoryScrolling({
+ anchorScrolling: 'enabled',
+ }),
+ ),
+ ],
};
diff --git a/src/app/layout/footer/footer.html b/src/app/layout/footer/footer.html
index 28c0d7d..e69de29 100644
--- a/src/app/layout/footer/footer.html
+++ b/src/app/layout/footer/footer.html
@@ -1 +0,0 @@
-
footer works!
diff --git a/src/app/layout/header/header.css b/src/app/layout/header/header.css
deleted file mode 100644
index e69de29..0000000
diff --git a/src/app/layout/header/header.html b/src/app/layout/header/header.html
index e69de29..9cd7b7d 100644
--- a/src/app/layout/header/header.html
+++ b/src/app/layout/header/header.html
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/layout/header/header.model.ts b/src/app/layout/header/header.model.ts
new file mode 100644
index 0000000..c0c2e38
--- /dev/null
+++ b/src/app/layout/header/header.model.ts
@@ -0,0 +1,13 @@
+export interface NavigationItem {
+ readonly label: string;
+ readonly fragment: string;
+ readonly hoverDecorationSrc: string;
+}
+
+export interface SocialLink {
+ readonly id: string;
+ readonly href: string;
+ readonly accessibleLabel: string;
+ readonly iconSrc: string;
+ readonly opensInNewTab: boolean;
+}
diff --git a/src/app/layout/header/header.spec.ts b/src/app/layout/header/header.spec.ts
deleted file mode 100644
index 9ef7403..0000000
--- a/src/app/layout/header/header.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { Header } from './header';
-
-describe('Header', () => {
- let component: Header;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [Header],
- }).compileComponents();
-
- fixture = TestBed.createComponent(Header);
- component = fixture.componentInstance;
- await fixture.whenStable();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/src/app/layout/header/header.ts b/src/app/layout/header/header.ts
index 6aa2613..43c35ef 100644
--- a/src/app/layout/header/header.ts
+++ b/src/app/layout/header/header.ts
@@ -1,9 +1,167 @@
-import { Component } from '@angular/core';
+import { DOCUMENT } from '@angular/common';
+import {
+ afterNextRender,
+ Component,
+ HostListener,
+ inject,
+ Injector,
+ signal,
+ viewChild,
+} from '@angular/core';
+import type { ElementRef, OnDestroy } from '@angular/core';
+import { RouterLink } from '@angular/router';
+
+import type { NavigationItem, SocialLink } from './header.model';
+
+const DESKTOP_MEDIA_QUERY = '(min-width: 64rem)';
@Component({
selector: 'app-header',
- imports: [],
+ imports: [RouterLink],
templateUrl: './header.html',
- styleUrl: './header.css',
})
-export class Header {}
+export class Header implements OnDestroy {
+ private readonly document = inject(DOCUMENT);
+ private readonly injector = inject(Injector);
+
+ private readonly openMenuButton =
+ viewChild.required>('openMenuButton');
+
+ private readonly closeMenuButton =
+ viewChild.required>('closeMenuButton');
+
+ private previousBodyOverflow = '';
+ private isBodyScrollLocked = false;
+
+ protected readonly activeLanguage = signal<'en' | 'de'>('en');
+ protected readonly isMobileMenuOpen = signal(false);
+
+ protected readonly navigationItems: readonly NavigationItem[] = [
+ {
+ label: 'About me',
+ fragment: 'about',
+ hoverDecorationSrc: '/assets/decorations/navigation/nav-hover-about.svg',
+ },
+ {
+ label: 'Skills',
+ fragment: 'skills',
+ hoverDecorationSrc: '/assets/decorations/navigation/nav-hover-skills.svg',
+ },
+ {
+ label: 'Projects',
+ fragment: 'projects',
+ hoverDecorationSrc: '/assets/decorations/navigation/nav-hover-projects.svg',
+ },
+ {
+ label: 'Contact',
+ fragment: 'contact',
+ hoverDecorationSrc: '/assets/decorations/navigation/nav-hover-contact.svg',
+ },
+ ];
+
+ protected readonly socialLinks: readonly SocialLink[] = [
+ {
+ id: 'linkedin',
+ href: 'https://www.linkedin.com/in/kamyarzamanfar/',
+ accessibleLabel: 'Visit LinkedIn profile',
+ iconSrc: '/assets/icons/social/linkedin.svg',
+ opensInNewTab: true,
+ },
+ {
+ id: 'github',
+ href: 'https://github.com/kamycoding',
+ accessibleLabel: 'Visit GitHub profile',
+ iconSrc: '/assets/icons/social/github.svg',
+ opensInNewTab: true,
+ },
+ {
+ id: 'email',
+ href: 'mailto:kamyar.zamanfar@gmail.com',
+ accessibleLabel: 'Send an email',
+ iconSrc: '/assets/icons/social/email.svg',
+ opensInNewTab: false,
+ },
+ ];
+
+ protected toggleLanguage(): void {
+ this.activeLanguage.update((language) => (language === 'en' ? 'de' : 'en'));
+ }
+
+ protected openMobileMenu(): void {
+ if (this.isMobileMenuOpen()) {
+ return;
+ }
+
+ this.isMobileMenuOpen.set(true);
+ this.lockBodyScroll();
+
+ afterNextRender(
+ () => {
+ if (this.isMobileMenuOpen()) {
+ this.closeMenuButton().nativeElement.focus({
+ preventScroll: true,
+ });
+ }
+ },
+ {
+ injector: this.injector,
+ },
+ );
+ }
+
+ protected closeMobileMenu(restoreFocus = true): void {
+ if (!this.isMobileMenuOpen()) {
+ return;
+ }
+
+ if (restoreFocus) {
+ this.openMenuButton().nativeElement.focus({
+ preventScroll: true,
+ });
+ }
+
+ this.isMobileMenuOpen.set(false);
+ this.restoreBodyScroll();
+ }
+
+ @HostListener('document:keydown.escape')
+ protected onEscape(): void {
+ if (!this.isMobileMenuOpen()) {
+ return;
+ }
+
+ this.closeMobileMenu();
+ }
+
+ @HostListener('window:resize')
+ protected onViewportResize(): void {
+ const isDesktop = this.document.defaultView?.matchMedia(DESKTOP_MEDIA_QUERY).matches ?? false;
+
+ if (isDesktop) {
+ this.closeMobileMenu(false);
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.restoreBodyScroll();
+ }
+
+ private lockBodyScroll(): void {
+ if (this.isBodyScrollLocked) {
+ return;
+ }
+
+ this.previousBodyOverflow = this.document.body.style.overflow;
+ this.document.body.style.overflow = 'hidden';
+ this.isBodyScrollLocked = true;
+ }
+
+ private restoreBodyScroll(): void {
+ if (!this.isBodyScrollLocked) {
+ return;
+ }
+
+ this.document.body.style.overflow = this.previousBodyOverflow;
+ this.isBodyScrollLocked = false;
+ }
+}
diff --git a/src/app/pages/home/home.html b/src/app/pages/home/home.html
index 0d9d7b5..e69de29 100644
--- a/src/app/pages/home/home.html
+++ b/src/app/pages/home/home.html
@@ -1,2 +0,0 @@
-
-Kamy :)