Redesign portfolio around systems case studies
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { CommonModule, NgOptimizedImage } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { SectionNavItem } from '../../shared/models/section-nav.model';
|
||||
import { ThemeToggleComponent } from '../../shared/ui/theme-toggle/theme-toggle.component';
|
||||
@@ -7,88 +7,49 @@ import { ThemeToggleComponent } from '../../shared/ui/theme-toggle/theme-toggle.
|
||||
@Component({
|
||||
selector: 'app-page-shell',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink, ThemeToggleComponent, NgOptimizedImage],
|
||||
imports: [CommonModule, RouterLink, ThemeToggleComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div
|
||||
class="min-h-screen bg-[#f4f7f5] text-zinc-800 font-saira transition-colors duration-300 dark:bg-slate-950 dark:text-zinc-100"
|
||||
>
|
||||
<header
|
||||
class="sticky top-0 z-30 border-b border-zinc-200/70 bg-[#f8faf7]/85 backdrop-blur transition-colors duration-300 dark:border-slate-900/60 dark:bg-slate-950/70"
|
||||
>
|
||||
<div
|
||||
class="mx-auto flex max-w-6xl items-center justify-between px-4 py-4 md:px-8"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
ngSrc="/images/profile.jpg"
|
||||
width="40"
|
||||
height="40"
|
||||
priority
|
||||
alt="Dzanan"
|
||||
class="h-10 w-10 rounded-full object-cover ring-1 ring-emerald-600/20 transition-colors dark:ring-emerald-500/30"
|
||||
/>
|
||||
<div class="flex flex-col leading-tight">
|
||||
<span class="text-sm text-zinc-500 dark:text-zinc-400"
|
||||
>Senior Software Engineer</span
|
||||
>
|
||||
<span class="text-base font-semibold">Amar Džanan</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-frame">
|
||||
<a class="skip-link" href="#main-content">Skip to content</a>
|
||||
<header class="site-header">
|
||||
<div class="shell header-inner">
|
||||
<a routerLink="/" class="wordmark" aria-label="Amar Džanan, home">
|
||||
<span class="wordmark-mark">AD/</span>
|
||||
<span class="wordmark-copy">Amar Džanan<small>Software systems & AI</small></span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop Nav & Toggle -->
|
||||
<div class="hidden items-center gap-4 md:flex">
|
||||
<nav class="flex items-center gap-2">
|
||||
<div class="desktop-nav">
|
||||
<nav aria-label="Primary navigation">
|
||||
@for (section of sections; track section.label) {
|
||||
<a
|
||||
[routerLink]="section.path || []"
|
||||
[fragment]="section.fragment"
|
||||
class="rounded-full px-3 py-2 text-sm font-medium text-zinc-600 transition hover:bg-emerald-50/80 hover:text-zinc-900 dark:text-zinc-200 dark:hover:bg-slate-900 dark:hover:text-white"
|
||||
>
|
||||
{{ section.label }}
|
||||
</a>
|
||||
<a [routerLink]="section.path || []" [fragment]="section.fragment">{{ section.label }}</a>
|
||||
}
|
||||
</nav>
|
||||
<div
|
||||
class="h-6 w-px bg-zinc-200 dark:bg-slate-800"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<app-theme-toggle />
|
||||
</div>
|
||||
|
||||
<!-- Mobile Toggle & Menu Placeholder -->
|
||||
<div class="flex items-center gap-4 md:hidden">
|
||||
<button class="menu-button" type="button" (click)="toggleMenu()" [attr.aria-expanded]="menuOpen()" aria-controls="mobile-navigation">
|
||||
<span>Menu</span><span aria-hidden="true">{{ menuOpen() ? '×' : '+' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@if (menuOpen()) {
|
||||
<div id="mobile-navigation" class="mobile-nav shell">
|
||||
<nav aria-label="Mobile navigation">
|
||||
@for (section of sections; track section.label) {
|
||||
<a [routerLink]="section.path || []" [fragment]="section.fragment" (click)="closeMenu()">{{ section.label }}</a>
|
||||
}
|
||||
</nav>
|
||||
<app-theme-toggle />
|
||||
<span class="text-sm text-zinc-500 dark:text-zinc-400"
|
||||
>Navigate</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
<main class="relative overflow-hidden opacity-0 animate-[fade-in-up_1s_ease-out_0.3s_forwards]">
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 opacity-60 transition-opacity duration-500 dark:opacity-60"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-x-0 top-0 h-96 bg-[radial-gradient(circle_at_50%_20%,rgba(16,185,129,0.1),transparent_42%)] dark:bg-[radial-gradient(circle_at_50%_20%,rgba(16,185,129,0.2),transparent_40%)]"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-y-0 right-0 w-1/2 bg-[radial-gradient(circle_at_80%_50%,rgba(14,165,233,0.07),transparent_38%)] dark:bg-[radial-gradient(circle_at_80%_50%,rgba(59,130,246,0.12),transparent_35%)]"
|
||||
></div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<ng-content />
|
||||
</div>
|
||||
<main id="main-content">
|
||||
<ng-content />
|
||||
</main>
|
||||
<footer
|
||||
class="border-t border-zinc-200/70 bg-[#f8faf7]/85 transition-colors duration-300 dark:border-slate-900/60 dark:bg-slate-950/80"
|
||||
>
|
||||
<div
|
||||
class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-4 px-4 py-6 text-sm text-zinc-500 dark:text-zinc-400 md:px-8"
|
||||
>
|
||||
<span>Built for clarity and craft.</span>
|
||||
<span class="text-zinc-600 dark:text-zinc-500">dzanan.net</span>
|
||||
<footer class="site-footer">
|
||||
<div class="shell footer-inner">
|
||||
<span>Amar Džanan · Sarajevo, Bosnia and Herzegovina</span>
|
||||
<div><a href="mailto:amar@dzanan.net">amar@dzanan.net</a><span aria-hidden="true"> / </span><a href="https://www.linkedin.com/in/amardzanan/">LinkedIn</a></div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -96,4 +57,13 @@ import { ThemeToggleComponent } from '../../shared/ui/theme-toggle/theme-toggle.
|
||||
})
|
||||
export class PageShellComponent {
|
||||
@Input({ required: true }) sections: SectionNavItem[] = [];
|
||||
readonly menuOpen = signal(false);
|
||||
|
||||
toggleMenu(): void {
|
||||
this.menuOpen.update(open => !open);
|
||||
}
|
||||
|
||||
closeMenu(): void {
|
||||
this.menuOpen.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
45
src/app/core/services/seo.service.ts
Normal file
45
src/app/core/services/seo.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Meta, Title } from '@angular/platform-browser';
|
||||
|
||||
export type SeoPage = {
|
||||
title: string;
|
||||
description: string;
|
||||
path: string;
|
||||
type?: 'website' | 'article';
|
||||
structuredData?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SeoService {
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly meta = inject(Meta);
|
||||
private readonly title = inject(Title);
|
||||
|
||||
update(page: SeoPage): void {
|
||||
const canonicalUrl = `https://dzanan.net${page.path}`;
|
||||
this.title.setTitle(page.title);
|
||||
this.meta.updateTag({ name: 'description', content: page.description });
|
||||
this.meta.updateTag({ property: 'og:title', content: page.title });
|
||||
this.meta.updateTag({ property: 'og:description', content: page.description });
|
||||
this.meta.updateTag({ property: 'og:type', content: page.type ?? 'website' });
|
||||
this.meta.updateTag({ property: 'og:url', content: canonicalUrl });
|
||||
|
||||
let canonical = this.document.head.querySelector<HTMLLinkElement>('link[rel="canonical"]');
|
||||
if (!canonical) {
|
||||
canonical = this.document.createElement('link');
|
||||
canonical.rel = 'canonical';
|
||||
this.document.head.appendChild(canonical);
|
||||
}
|
||||
canonical.href = canonicalUrl;
|
||||
|
||||
this.document.head.querySelector('script[data-page-structured-data]')?.remove();
|
||||
if (page.structuredData) {
|
||||
const script = this.document.createElement('script');
|
||||
script.type = 'application/ld+json';
|
||||
script.setAttribute('data-page-structured-data', '');
|
||||
script.textContent = JSON.stringify(page.structuredData);
|
||||
this.document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user