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 @@ +
+ +
+ + + + + + + +
+ + + +
+ + +
+
+ + + + + +
+ +
+ + + + + +
+ + +
+ @for (item of socialLinks; track item.id) { + + + + } +
+
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 @@ -

Frontend

-

Kamy :)