diff --git a/libs/feature/about-me/src/lib/hero-content/hero-content.component.html b/libs/feature/about-me/src/lib/hero-content/hero-content.component.html index 3d17121..898f02a 100644 --- a/libs/feature/about-me/src/lib/hero-content/hero-content.component.html +++ b/libs/feature/about-me/src/lib/hero-content/hero-content.component.html @@ -28,9 +28,16 @@

Oliver Stadie

}

- 25 years of experience in software development, well-organized, with a - broad technical perspective and a strong sense of ownership. Focused on - aligning technical solutions with business objectives. + 25 years of experience in software development, + well-organized, with a broad technical perspective and a strong sense of + ownership. Focused on aligning technical solutions with business + objectives.

diff --git a/libs/feature/about-me/src/lib/hero-content/hero-content.component.ts b/libs/feature/about-me/src/lib/hero-content/hero-content.component.ts index 8e60e27..89b54ec 100644 --- a/libs/feature/about-me/src/lib/hero-content/hero-content.component.ts +++ b/libs/feature/about-me/src/lib/hero-content/hero-content.component.ts @@ -4,6 +4,8 @@ import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; import { CustomizationStateService } from '@portfolio/customization-state'; +import { HeroHiddenLinkService } from './hero-hidden-link.service'; + @Component({ selector: 'lib-hero-content', host: { '[class.print-mode]': 'isPrintMode()' }, @@ -12,6 +14,11 @@ import { CustomizationStateService } from '@portfolio/customization-state'; styleUrl: './hero-content.component.scss', }) export class HeroContentComponent { + private readonly hiddenLinkService = inject(HeroHiddenLinkService); + protected readonly isPrintMode = inject(CustomizationStateService) .isPrintMode; + + protected readonly hiddenLinkUrl = () => + this.hiddenLinkService.getHiddenLinkUrl(); } diff --git a/libs/feature/about-me/src/lib/hero-content/hero-hidden-link.service.spec.ts b/libs/feature/about-me/src/lib/hero-content/hero-hidden-link.service.spec.ts new file mode 100644 index 0000000..e887e75 --- /dev/null +++ b/libs/feature/about-me/src/lib/hero-content/hero-hidden-link.service.spec.ts @@ -0,0 +1,129 @@ +import { DOCUMENT } from '@angular/common'; +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { HeroHiddenLinkService } from './hero-hidden-link.service'; + +describe('HeroHiddenLinkService', () => { + function createService(params: { + href?: string; + routerUrl?: string; + }): HeroHiddenLinkService { + TestBed.configureTestingModule({ + providers: [ + HeroHiddenLinkService, + { + provide: DOCUMENT, + useValue: { + location: { + href: params.href, + }, + }, + }, + { + provide: Router, + useValue: { + url: params.routerUrl ?? '/', + }, + }, + ], + }); + + return TestBed.inject(HeroHiddenLinkService); + } + + it('removes customizationPanelShown and keeps all other URL parts', () => { + const service = createService({ + href: 'https://oliverstadie.com/?customizationPanelShown=true&searchTags=React&printMode=true&skillMatrixExperienceUnit=time&projectSortOrder=date&printProjectPageSizes=3,5,5#projects', + }); + + const parsed = new URL(service.getHiddenLinkUrl()); + + expect(parsed.origin).toBe('https://oliverstadie.com'); + expect(parsed.hash).toBe(''); + expect(parsed.searchParams.get('customizationPanelShown')).toBeNull(); + expect(parsed.searchParams.get('searchTags')).toBe('React'); + expect(parsed.searchParams.get('printMode')).toBe('true'); + }); + + it('replaces localhost origin with production origin', () => { + const service = createService({ + href: 'http://localhost:4200/?customizationPanelShown=true&searchTags=React&printMode=true&skillMatrixExperienceUnit=time&projectSortOrder=date&printProjectPageSizes=3,5,5#projects', + }); + + const parsed = new URL(service.getHiddenLinkUrl()); + + expect(parsed.origin).toBe('https://oliverstadie.com'); + expect(parsed.hash).toBe(''); + expect(parsed.searchParams.get('customizationPanelShown')).toBeNull(); + }); + + it('keeps non-localhost origin unchanged', () => { + const service = createService({ + href: 'https://preview.oliverstadie.dev/?customizationPanelShown=true&searchTags=React#projects', + }); + + const parsed = new URL(service.getHiddenLinkUrl()); + + expect(parsed.origin).toBe('https://preview.oliverstadie.dev'); + expect(parsed.hash).toBe(''); + expect(parsed.searchParams.get('customizationPanelShown')).toBeNull(); + expect(parsed.searchParams.get('searchTags')).toBe('React'); + }); + + it('keeps URL unchanged when customizationPanelShown is not present', () => { + const service = createService({ + href: 'https://oliverstadie.com/?searchTags=React&printMode=true&skillMatrixExperienceUnit=time&projectSortOrder=date&printProjectPageSizes=3,5,5#projects', + }); + + const parsed = new URL(service.getHiddenLinkUrl()); + + expect(parsed.origin).toBe('https://oliverstadie.com'); + expect(parsed.hash).toBe(''); + expect(parsed.searchParams.get('searchTags')).toBe('React'); + expect(parsed.searchParams.get('printMode')).toBe('true'); + }); + + it('removes customizationPanelShown when it is false', () => { + const service = createService({ + href: 'https://oliverstadie.com/?customizationPanelShown=false&searchTags=React&printMode=true&skillMatrixExperienceUnit=time&projectSortOrder=date&printProjectPageSizes=3,5,5#projects', + }); + + const parsed = new URL(service.getHiddenLinkUrl()); + + expect(parsed.origin).toBe('https://oliverstadie.com'); + expect(parsed.hash).toBe(''); + expect(parsed.searchParams.get('customizationPanelShown')).toBeNull(); + expect(parsed.searchParams.get('searchTags')).toBe('React'); + }); + + it('falls back to router url when location href is missing', () => { + const service = createService({ + href: undefined, + routerUrl: + '/?customizationPanelShown=true&searchTags=React&printMode=true&skillMatrixExperienceUnit=time&projectSortOrder=date&printProjectPageSizes=3,5,5#projects', + }); + + const parsed = new URL(service.getHiddenLinkUrl()); + + expect(parsed.origin).toBe('https://oliverstadie.com'); + expect(parsed.pathname).toBe('/'); + expect(parsed.hash).toBe(''); + expect(parsed.searchParams.get('customizationPanelShown')).toBeNull(); + expect(parsed.searchParams.get('searchTags')).toBe('React'); + }); + + it('creates an absolute URL from router url fallback without relative path assumptions', () => { + const service = createService({ + href: undefined, + routerUrl: + '/?customizationPanelShown=true&searchTags=React&printMode=true&skillMatrixExperienceUnit=time&projectSortOrder=date&printProjectPageSizes=3,5,5', + }); + + const parsed = new URL(service.getHiddenLinkUrl()); + + expect(parsed.origin).toBe('https://oliverstadie.com'); + expect(parsed.pathname).toBe('/'); + expect(parsed.hash).toBe(''); + }); +}); diff --git a/libs/feature/about-me/src/lib/hero-content/hero-hidden-link.service.ts b/libs/feature/about-me/src/lib/hero-content/hero-hidden-link.service.ts new file mode 100644 index 0000000..cdbf68d --- /dev/null +++ b/libs/feature/about-me/src/lib/hero-content/hero-hidden-link.service.ts @@ -0,0 +1,61 @@ +import { DOCUMENT } from '@angular/common'; +import { inject, Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { WEB_METADATA } from '@portfolio/web-metadata'; + +@Injectable({ + providedIn: 'root', +}) +export class HeroHiddenLinkService { + private static readonly PANEL_QUERY_PARAM = 'customizationPanelShown'; + + private readonly document = inject(DOCUMENT); + private readonly router = inject(Router); + + getHiddenLinkUrl(): string { + const currentUrl = this.getCurrentUrl(); + + currentUrl.searchParams.delete(HeroHiddenLinkService.PANEL_QUERY_PARAM); + currentUrl.hash = ''; + + if (this.isLocalhost(currentUrl.hostname)) { + this.replaceOriginWithPublicSiteOrigin(currentUrl); + } + + return currentUrl.toString(); + } + + private getCurrentUrl(): URL { + const href = this.document.location?.href; + + if (href && href !== 'null') { + try { + return new URL(href); + } catch { + return this.createAbsoluteUrlFromRouterUrl(); + } + } + + return this.createAbsoluteUrlFromRouterUrl(); + } + + private createAbsoluteUrlFromRouterUrl(): URL { + return new URL(this.router.url || '/', WEB_METADATA.defaults.siteOrigin); + } + + private replaceOriginWithPublicSiteOrigin(url: URL): void { + const publicOrigin = new URL(WEB_METADATA.defaults.siteOrigin); + + url.protocol = publicOrigin.protocol; + url.hostname = publicOrigin.hostname; + url.port = publicOrigin.port; + } + + private isLocalhost(hostname: string): boolean { + return ( + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname === '[::1]' + ); + } +}