Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ <h1 class="name">Oliver Stadie</h1>
}
</div>
<p class="hero-summary">
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<a
class="hidden-link"
target="_blank"
rel="noopener noreferrer"
[href]="hiddenLinkUrl()"
>,</a
>
well-organized, with a broad technical perspective and a strong sense of
ownership. Focused on aligning technical solutions with business
objectives.
</p>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()' },
Expand All @@ -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();
}
Original file line number Diff line number Diff line change
@@ -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('');
});
});
Original file line number Diff line number Diff line change
@@ -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]'
);
}
}
Loading