117 lines
5.6 KiB
TypeScript
Executable File
117 lines
5.6 KiB
TypeScript
Executable File
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|
import { RouterLink } from '@angular/router';
|
|
import { PageShellComponent } from '../../core/layout/page-shell.component';
|
|
import { SeoService } from '../../core/services/seo.service';
|
|
import { MAIN_NAVIGATION, PAGE_INDEX } from '../../shared/config/navigation.config';
|
|
|
|
type ProfileValue = string | number | readonly string[];
|
|
type TokenClass = 'k' | 's' | 'n' | 'p';
|
|
interface TerminalToken { readonly cls: TokenClass; readonly text: string; }
|
|
interface TerminalLine { readonly tokens: readonly TerminalToken[]; }
|
|
|
|
/** The `profile.json` shown in the hero. Order is render order. */
|
|
const PROFILE: readonly (readonly [string, ProfileValue])[] = [
|
|
['name', 'Amar Džanan'],
|
|
['role', 'Senior Full-Stack Engineer · Product Owner'],
|
|
['stack', ['.NET', 'Angular', 'Ionic', 'SQL Server']],
|
|
['focus', ['ERP & business systems', 'mobile', 'expert systems', 'safe AI integration']],
|
|
['since', 2018],
|
|
['location', 'Ljubljana · CET'],
|
|
['open_to', ['senior roles', 'consulting']]
|
|
];
|
|
|
|
function toTerminalLines(profile: readonly (readonly [string, ProfileValue])[]): TerminalLine[] {
|
|
return profile.map(([key, value], index) => {
|
|
const tokens: TerminalToken[] = [{ cls: 'k', text: `"${key}"` }, { cls: 'p', text: ': ' }];
|
|
if (typeof value === 'number') {
|
|
tokens.push({ cls: 'n', text: String(value) });
|
|
} else if (typeof value === 'string') {
|
|
tokens.push({ cls: 's', text: `"${value}"` });
|
|
} else {
|
|
tokens.push({ cls: 'p', text: '[' });
|
|
value.forEach((item, itemIndex) => {
|
|
if (itemIndex > 0) tokens.push({ cls: 'p', text: ', ' });
|
|
tokens.push({ cls: 's', text: `"${item}"` });
|
|
});
|
|
tokens.push({ cls: 'p', text: ']' });
|
|
}
|
|
if (index < profile.length - 1) tokens.push({ cls: 'p', text: ',' });
|
|
return { tokens };
|
|
});
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-landing-page',
|
|
standalone: true,
|
|
imports: [PageShellComponent, RouterLink],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
template: `
|
|
<app-page-shell [sections]="sections">
|
|
<section class="hero shell" aria-labelledby="hero-title">
|
|
<div class="hero-copy">
|
|
<p class="label">Senior full-stack engineer<span class="label-extra"> · product owner</span></p>
|
|
<h1 id="hero-title">Complex systems, <span>understood before changed.</span></h1>
|
|
<p class="hero-lead">I build and modernize full-stack business systems — ERP software, offline-first mobile apps for the field, rule-driven expert systems — and connect them to AI through narrow, validated, verified interfaces. The kind you can run in production and sleep at night.</p>
|
|
<div class="hero-actions">
|
|
<a class="btn btn-accent" href="mailto:amar@dzanan.net">amar@dzanan.net</a>
|
|
<a class="btn btn-line" routerLink="/cv">cv.pdf →</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel terminal" data-testid="profile-terminal" role="img" aria-label="Profile summary rendered as a JSON document">
|
|
<div class="terminal-bar"><div class="terminal-dots" aria-hidden="true"><span></span><span></span><span></span></div><span class="terminal-title">profile.json</span><span class="terminal-spacer"></span></div>
|
|
<div class="terminal-body">
|
|
<div class="terminal-line"><span class="c">$ </span>whoami<span class="c"> --json</span></div>
|
|
<div class="terminal-line">{{ '{' }}</div>
|
|
@for (line of terminalLines; track $index) {
|
|
<div class="terminal-line indent">@for (token of line.tokens; track $index) {<span [class]="token.cls">{{ token.text }}</span>}</div>
|
|
}
|
|
<div class="terminal-line">{{ '}' }}</div>
|
|
<div class="terminal-line prompt"><span class="c">$ </span><span class="cursor" aria-hidden="true"></span></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="section tight shell" aria-labelledby="index-title">
|
|
<div class="section-head padded">
|
|
<div><span class="label">// index</span><h2 id="index-title">ls ~/</h2></div>
|
|
<span class="count">{{ pages.length }} entries</span>
|
|
</div>
|
|
<div class="panel index-list">
|
|
@for (page of pages; track page.path) {
|
|
<a data-testid="index-row" class="index-row" [routerLink]="page.path">
|
|
<span class="name">{{ page.name }}</span>
|
|
<p>{{ page.description }}</p>
|
|
<span class="project-arrow" aria-hidden="true">→</span>
|
|
</a>
|
|
}
|
|
</div>
|
|
</section>
|
|
</app-page-shell>
|
|
`
|
|
})
|
|
export class LandingPageComponent {
|
|
readonly sections = MAIN_NAVIGATION;
|
|
readonly pages = PAGE_INDEX;
|
|
readonly terminalLines = toTerminalLines(PROFILE);
|
|
|
|
constructor() {
|
|
inject(SeoService).update({
|
|
title: 'Amar Džanan — Senior Software Engineer & AI Integration',
|
|
description: 'Senior full-stack engineer building and modernizing ERP, mobile and expert systems — and connecting them safely to AI.',
|
|
path: '/',
|
|
structuredData: {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Person',
|
|
name: 'Amar Džanan',
|
|
url: 'https://dzanan.net/',
|
|
email: 'mailto:amar@dzanan.net',
|
|
jobTitle: 'Senior Software Engineer',
|
|
sameAs: ['https://www.linkedin.com/in/amardzanan/'],
|
|
address: { '@type': 'PostalAddress', addressLocality: 'Ljubljana', addressCountry: 'SI' },
|
|
knowsAbout: ['.NET', 'Angular', 'TypeScript', 'Ionic', 'ERP software', 'Expert systems', 'Software architecture', 'Model Context Protocol', 'AI integration']
|
|
}
|
|
});
|
|
}
|
|
}
|