first commit
All checks were successful
Deploy dzanan.net / deploy (push) Successful in 1m2s

This commit is contained in:
v7
2026-05-14 20:58:16 +02:00
commit 1599615ec7
48 changed files with 13000 additions and 0 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
node_modules
dist
.angular
.git
.gitea
npm-debug.log
coverage

17
.editorconfig Normal file
View File

@@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@@ -0,0 +1,61 @@
name: Deploy dzanan.net
on:
push:
branches: [main]
jobs:
deploy:
runs-on: behemoth
steps:
- name: Checkout
run: |
podman exec podman_gitea_1 git --git-dir=/data/git/repositories/v7/dzanan.net.git archive "${GITHUB_SHA}" | tar -x
- name: Build image
run: |
podman build --pull -t localhost/dzanan-net:${GITHUB_SHA} -t localhost/dzanan-net:latest .
- name: Replace container
run: |
podman stop dzanan-web || true
podman rm dzanan-web || true
podman run -d \
--name dzanan-web \
--restart=unless-stopped \
--network podman_proxy \
-e NODE_ENV=production \
-e PORT=3000 \
localhost/dzanan-net:latest
- name: Check application health
run: |
podman exec dzanan-web node -e "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
- name: Configure Caddy route
run: |
python3 - <<'PY'
from pathlib import Path
path = Path("/home/podman/configs/caddy/Caddyfile")
text = path.read_text()
static = """dzanan.net, www.dzanan.net {
root * /srv/dzanan.net
file_server
encode gzip zstd
}"""
proxy = """dzanan.net, www.dzanan.net {
encode gzip zstd
reverse_proxy dzanan-web:3000
}"""
if static in text:
path.write_text(text.replace(static, proxy))
elif proxy not in text:
raise SystemExit("Expected dzanan.net Caddy block was not found")
PY
- name: Reload Caddy
run: |
podman exec podman_caddy_1 caddy validate --config /etc/caddy/Caddyfile
podman exec podman_caddy_1 caddy reload --config /etc/caddy/Caddyfile

42
.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

5
.postcssrc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

4
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

22
Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-alpine
ENV NODE_ENV=production
ENV PORT=3000
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/dzanan.net/server/server.mjs"]

59
README.md Normal file
View File

@@ -0,0 +1,59 @@
# DzananNet
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.1.4.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

96
angular.json Normal file
View File

@@ -0,0 +1,96 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"dzanan.net": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"server": "src/main.server.ts",
"outputMode": "server",
"ssr": {
"entry": "src/server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "dzanan.net:build:production"
},
"development": {
"buildTarget": "dzanan.net:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
]
}
}
}
}
}
}

11473
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

56
package.json Normal file
View File

@@ -0,0 +1,56 @@
{
"name": "dzanan.net",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"serve:ssr:dzanan.net": "node dist/dzanan.net/server/server.mjs"
},
"prettier": {
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
},
"private": true,
"dependencies": {
"@angular/cdk": "^20.2.14",
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"@angular/platform-server": "^21.0.0",
"@angular/router": "^21.0.0",
"@angular/ssr": "^21.0.0",
"@tailwindcss/postcss": "^4.1.17",
"express": "^5.1.0",
"postcss": "^8.5.6",
"rxjs": "~7.8.0",
"tailwindcss": "^4.1.17",
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular/build": "^21.0.0",
"@angular/cli": "^21.0.0",
"@angular/compiler-cli": "^21.0.0",
"@types/express": "^5.0.1",
"@types/jasmine": "~5.1.0",
"@types/node": "^20.17.19",
"jasmine-core": "~5.8.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.5.3",
"typescript": "~5.9.3"
}
}

BIN
public/duck.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/images/profile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
public/images/timeline.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,12 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(withRoutes(serverRoutes))
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

19
src/app/app.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(
routes,
withInMemoryScrolling({
anchorScrolling: 'enabled',
scrollPositionRestoration: 'enabled'
})
),
provideClientHydration(withEventReplay())
]
};

1
src/app/app.html Normal file
View File

@@ -0,0 +1 @@
<router-outlet />

View File

@@ -0,0 +1,8 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Prerender
}
];

17
src/app/app.routes.ts Normal file
View File

@@ -0,0 +1,17 @@
import { Routes } from '@angular/router';
import { LandingPageComponent } from './features/landing/landing-page.component';
export const routes: Routes = [
{
path: '',
component: LandingPageComponent
},
{
path: 'cv',
loadComponent: () => import('./features/cv/cv-page.component').then(m => m.CvPageComponent)
},
{
path: 'portfolio',
loadComponent: () => import('./features/portfolio/portfolio-page.component').then(m => m.PortfolioPageComponent)
}
];

14
src/app/app.ts Normal file
View File

@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ThemeService } from './core/services/theme.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './app.html'
})
export class App {
private readonly themeService = inject(ThemeService);
}

View File

@@ -0,0 +1,99 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { CommonModule, NgOptimizedImage } 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';
@Component({
selector: 'app-page-shell',
standalone: true,
imports: [CommonModule, RouterLink, ThemeToggleComponent, NgOptimizedImage],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div
class="min-h-screen bg-zinc-50 text-zinc-900 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/80 bg-white/70 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>
<!-- Desktop Nav & Toggle -->
<div class="hidden items-center gap-4 md:flex">
<nav class="flex items-center gap-2">
@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-zinc-100 hover:text-zinc-900 dark:text-zinc-200 dark:hover:bg-slate-900 dark:hover:text-white"
>
{{ 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">
<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.15),transparent_40%)] 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(59,130,246,0.1),transparent_35%)] 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>
<footer
class="border-t border-zinc-200/80 bg-white/80 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>
</div>
</footer>
</div>
`
})
export class PageShellComponent {
@Input({ required: true }) sections: SectionNavItem[] = [];
}

View File

@@ -0,0 +1,59 @@
import { Injectable, signal, effect, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
export type Theme = 'light' | 'dark' | 'system';
@Injectable({
providedIn: 'root'
})
export class ThemeService {
private readonly platformId = inject(PLATFORM_ID);
readonly theme = signal<Theme>('system');
constructor() {
if (isPlatformBrowser(this.platformId)) {
// Load saved theme
const savedTheme = localStorage.getItem('theme') as Theme;
if (savedTheme) {
this.theme.set(savedTheme);
}
// Apply theme effect
effect(() => {
const currentTheme = this.theme();
const root = window.document.documentElement;
localStorage.setItem('theme', currentTheme);
if (currentTheme === 'system') {
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (systemDark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
} else if (currentTheme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
});
// Listen for system changes if in system mode
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (this.theme() === 'system') {
if (e.matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
});
}
}
setTheme(theme: Theme) {
this.theme.set(theme);
}
}

View File

@@ -0,0 +1,85 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SectionComponent } from '../../shared/ui/section/section.component';
@Component({
selector: 'app-about-it-section',
standalone: true,
imports: [CommonModule, SectionComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<app-section
[id]="sectionId"
eyebrow="About IT"
title="Thoughtful engineering over novelty"
lead="I gravitate toward choices that keep the codebase predictable, observable, and easy to evolve."
>
<div class="grid gap-6 lg:grid-cols-3">
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">
Architecture
</p>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
Lean compositions, clear data boundaries, and contracts that are
easy to test. Small, composable modules beat monolith components.
</p>
</div>
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">Front end</p>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
Signals for straightforward state, typed APIs, accessibility baked
into the UI, and styling that stays aligned with design systems.
</p>
</div>
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">
Delivery
</p>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
Automated checks, preview builds, and incremental releases over
big-bang launches. Measure impact; iterate with feedback.
</p>
</div>
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">
Backend Integration
</p>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
Seamless integration with backend services, optimizing data flow, and ensuring efficient full-stack performance.
</p>
</div>
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">
API Contracts
</p>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
Defining clear, strongly-typed API contracts to ensure reliability, improve developer experience, and reduce integration friction.
</p>
</div>
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">
Client Handling
</p>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-300">
Proactive communication, understanding client needs, translating requirements into technical specifications, and ensuring timely delivery with clear expectations.
</p>
</div>
</div>
</app-section>
`
})
export class AboutItSectionComponent {
readonly sectionId = 'about-it';
}

View File

@@ -0,0 +1,60 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SectionComponent } from '../../shared/ui/section/section.component';
@Component({
selector: 'app-about-me-section',
standalone: true,
imports: [CommonModule, SectionComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<app-section
[id]="sectionId"
eyebrow="About me"
title="Engineer, collaborator, perpetual learner"
lead="I like to keep teams moving: establish a clear plan, align stakeholders, and turn good ideas into shipped, measurable outcomes."
>
<div class="grid gap-8 lg:grid-cols-2">
<div class="space-y-4 text-zinc-600 dark:text-zinc-200">
<p>
I care about pairing good taste with solid engineering practices.
Whether building a new product or refining an existing one, I work
to keep the codebase tidy, the UI cohesive, and the delivery
predictable.
</p>
<p>
I thrive in cross-functional teams, translating between design,
product, and engineering to keep efforts aligned and transparent.
</p>
</div>
<div class="grid gap-3">
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">
Working principles
</p>
<ul class="mt-2 space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
<li>• Design for clarity, then refine for speed.</li>
<li>• Prefer simple interfaces over clever abstractions.</li>
<li>• Optimize for maintainability and team velocity.</li>
</ul>
</div>
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">Ways I help</p>
<ul class="mt-2 space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
<li>• Structuring design systems and component libraries.</li>
<li>• Improving developer experience and tooling.</li>
<li>• Coaching teams on pragmatic testing and reviews.</li>
</ul>
</div>
</div>
</div>
</app-section>
`
})
export class AboutMeSectionComponent {
readonly sectionId = 'about-me';
}

View File

@@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { PageShellComponent } from '../../core/layout/page-shell.component';
import { CvSectionComponent } from './cv-section.component';
import { MAIN_NAVIGATION } from '../../shared/config/navigation.config';
@Component({
selector: 'app-cv-page',
standalone: true,
imports: [PageShellComponent, CvSectionComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<app-page-shell [sections]="sections">
<app-cv-section />
</app-page-shell>
`
})
export class CvPageComponent {
readonly sections = MAIN_NAVIGATION;
}

View File

@@ -0,0 +1,80 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SectionComponent } from '../../shared/ui/section/section.component';
@Component({
selector: 'app-cv-section',
standalone: true,
imports: [CommonModule, SectionComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<app-section
[id]="sectionId"
eyebrow="CV"
title="Snapshot of my experience"
lead="Highlights of how I work and where I add value. Full CV is ready to share on request."
>
<div class="grid gap-6 lg:grid-cols-3">
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">
Experience focus
</p>
<ul class="mt-2 space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
<li>• Leading front-end builds for product teams.</li>
<li>• Partnering with design on component systems.</li>
<li>• Modernizing legacy code into maintainable stacks.</li>
<li>• Coaching teams on delivery practices and reviews.</li>
</ul>
</div>
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">Skills & tools</p>
<div class="mt-3 flex flex-wrap gap-2 text-xs">
<span class="rounded-full bg-zinc-100 px-3 py-1 text-zinc-700 dark:bg-slate-800/80 dark:text-zinc-200">
Angular
</span>
<span class="rounded-full bg-zinc-100 px-3 py-1 text-zinc-700 dark:bg-slate-800/80 dark:text-zinc-200">
TypeScript
</span>
<span class="rounded-full bg-zinc-100 px-3 py-1 text-zinc-700 dark:bg-slate-800/80 dark:text-zinc-200">
Node.js
</span>
<span class="rounded-full bg-zinc-100 px-3 py-1 text-zinc-700 dark:bg-slate-800/80 dark:text-zinc-200">
Tailwind
</span>
<span class="rounded-full bg-zinc-100 px-3 py-1 text-zinc-700 dark:bg-slate-800/80 dark:text-zinc-200">
Design systems
</span>
<span class="rounded-full bg-zinc-100 px-3 py-1 text-zinc-700 dark:bg-slate-800/80 dark:text-zinc-200">
Testing
</span>
<span class="rounded-full bg-zinc-100 px-3 py-1 text-zinc-700 dark:bg-slate-800/80 dark:text-zinc-200">
CI/CD
</span>
</div>
</div>
<div
class="rounded-xl border border-zinc-200 bg-white/60 p-4 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<p class="text-sm font-semibold text-emerald-600 dark:text-emerald-300">
How I structure work
</p>
<ul class="mt-2 space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
<li>• Clarify requirements with fast prototypes.</li>
<li>• Ship in small increments with measurable outcomes.</li>
<li>• Automate tests and checks to keep releases calm.</li>
<li>
• Document interfaces and patterns for future teammates.
</li>
</ul>
</div>
</div>
</app-section>
`
})
export class CvSectionComponent {
readonly sectionId = 'cv';
}

View File

@@ -0,0 +1,146 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SectionComponent } from '../../shared/ui/section/section.component';
type TimelineSide = 'left' | 'right';
type TimelineEntry = {
step: string;
phase: string;
title: string;
subtitle: string;
side: TimelineSide;
pulsating?: boolean; // Make pulsating an optional boolean property
};
@Component({
selector: 'app-home-section',
standalone: true,
imports: [CommonModule, SectionComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<app-section
[id]="sectionId"
eyebrow="Career Journey"
title="Building dependable software that feels effortless"
lead="Heres a timeline of my professional growth and key milestones, highlighting significant roles and contributions."
>
<div
class="overflow-hidden rounded-3xl border border-zinc-200 bg-white/60 shadow-xl shadow-emerald-500/5 backdrop-blur-sm transition-colors duration-300 dark:border-slate-800/60 dark:bg-slate-950/40 dark:shadow-emerald-900/20"
>
<div class="relative px-4 py-16 md:px-12">
<!-- Central Line -->
<div
class="absolute left-8 top-0 h-full w-px -translate-x-1/2 bg-gradient-to-b from-transparent via-zinc-300 to-transparent transition-colors duration-300 dark:via-slate-700 md:left-1/2"
aria-hidden="true"
></div>
<div class="flex flex-col gap-12 md:gap-20">
@for (entry of timeline; track entry.title) {
<div
class="group relative grid grid-cols-[2rem_1fr] gap-8 md:grid-cols-[1fr_auto_1fr] md:items-center md:gap-0"
>
<!-- Left Side Content (Desktop) -->
<div class="hidden md:block md:col-start-1 md:pr-12 md:text-right">
@if (entry.side === 'left') {
<div class="relative">
<span class="mb-2 inline-block text-xs font-bold uppercase tracking-widest text-emerald-600/90 dark:text-emerald-500/80">
{{ entry.step }} &bull; {{ entry.phase }}
</span>
<h3 class="mb-2 text-xl font-semibold text-zinc-900 transition-colors group-hover:text-emerald-600 dark:text-slate-100 dark:group-hover:text-emerald-400">
{{ entry.title }}
</h3>
<p class="text-base text-zinc-600 leading-relaxed dark:text-slate-400">
{{ entry.subtitle }}
</p>
</div>
}
</div>
<!-- Center Marker -->
<div class="relative col-start-1 flex items-center justify-center md:col-start-2">
<div class="relative flex h-3 w-3 items-center justify-center">
@if (entry.pulsating) {
<span
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75"
></span>
}
<div
class="relative h-3 w-3 rounded-full bg-emerald-500 ring-4 ring-white shadow-[0_0_12px_rgba(16,185,129,0.6)] transition-all duration-300 group-hover:scale-125 dark:ring-slate-950"
></div>
</div>
</div>
<!-- Right Side Content (Desktop) & Mobile Content (All) -->
<div class="col-start-2 md:col-start-3 md:pl-12">
@if (entry.side === 'right') {
<div class="relative">
<span class="mb-2 inline-block text-xs font-bold uppercase tracking-widest text-emerald-600/90 dark:text-emerald-500/80">
{{ entry.step }} &bull; {{ entry.phase }}
</span>
<h3 class="mb-2 text-xl font-semibold text-zinc-900 transition-colors group-hover:text-emerald-600 dark:text-slate-100 dark:group-hover:text-emerald-400">
{{ entry.title }}
</h3>
<p class="text-base text-zinc-600 leading-relaxed dark:text-slate-400">
{{ entry.subtitle }}
</p>
</div>
}
@else {
<!-- Mobile only view for 'left' entries -->
<div class="block md:hidden">
<span class="mb-2 inline-block text-xs font-bold uppercase tracking-widest text-emerald-600/90 dark:text-emerald-500/80">
{{ entry.step }} &bull; {{ entry.phase }}
</span>
<h3 class="mb-2 text-xl font-semibold text-zinc-900 transition-colors group-hover:text-emerald-600 dark:text-slate-100 dark:group-hover:text-emerald-400">
{{ entry.title }}
</h3>
<p class="text-base text-zinc-600 leading-relaxed dark:text-slate-400">
{{ entry.subtitle }}
</p>
</div>
}
</div>
</div>
}
</div>
</div>
</div>
</app-section>
`
})
export class HomeSectionComponent {
readonly sectionId = 'home';
readonly timeline: TimelineEntry[] = [
{
step: '2000 - 2010',
phase: 'COMP-2000 Ltd',
title: 'Junior Developer',
subtitle: 'Started my career focusing on foundational programming skills and contributing to small-scale projects.',
side: 'left'
},
{
step: '2010 - 2020',
phase: 'COMP-2000 Ltd',
title: 'Senior Developer',
subtitle: 'Led development teams, implemented complex features, and mentored junior staff, specializing in backend systems.',
side: 'right'
},
{
step: '2020 - 2024',
phase: 'COMP-2000 Ltd',
title: 'Technical Lead',
subtitle: 'Architected scalable solutions and managed the technical roadmap for critical product lines.',
side: 'left'
},
{
step: '2024 - Present',
phase: 'Unija ETL Group',
title: 'Senior Software Engineer',
subtitle: 'Currently focused on developing robust ETL processes and data integration solutions within a dynamic financial environment.',
side: 'right',
pulsating: true
}
];
}

View File

@@ -0,0 +1,28 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { PageShellComponent } from '../../core/layout/page-shell.component';
import { HomeSectionComponent } from '../home/home-section.component';
import { AboutMeSectionComponent } from '../about/about-me-section.component';
import { AboutItSectionComponent } from '../about/about-it-section.component';
import { MAIN_NAVIGATION } from '../../shared/config/navigation.config';
@Component({
selector: 'app-landing-page',
standalone: true,
imports: [
PageShellComponent,
HomeSectionComponent,
AboutMeSectionComponent,
AboutItSectionComponent
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<app-page-shell [sections]="sections">
<app-home-section />
<app-about-me-section />
<app-about-it-section />
</app-page-shell>
`
})
export class LandingPageComponent {
readonly sections = MAIN_NAVIGATION;
}

View File

@@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { PageShellComponent } from '../../core/layout/page-shell.component';
import { PortfolioSectionComponent } from './portfolio-section.component';
import { MAIN_NAVIGATION } from '../../shared/config/navigation.config';
@Component({
selector: 'app-portfolio-page',
standalone: true,
imports: [PageShellComponent, PortfolioSectionComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<app-page-shell [sections]="sections">
<app-portfolio-section />
</app-page-shell>
`
})
export class PortfolioPageComponent {
readonly sections = MAIN_NAVIGATION;
}

View File

@@ -0,0 +1,86 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SectionComponent } from '../../shared/ui/section/section.component';
type PortfolioItem = {
title: string;
summary: string;
period: string;
tags: string[];
};
@Component({
selector: 'app-portfolio-section',
standalone: true,
imports: [CommonModule, SectionComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<app-section
[id]="sectionId"
eyebrow="Portfolio"
title="Representative work"
lead="A few examples of the kind of outcomes I like to deliver. Swap in your own case studies here."
>
<div class="grid gap-6 md:grid-cols-2">
@for (item of items; track item.title) {
<article
class="flex h-full flex-col gap-3 rounded-xl border border-zinc-200 bg-white/60 p-5 dark:border-slate-800/60 dark:bg-slate-900/40"
>
<div class="flex items-center justify-between text-sm text-zinc-500 dark:text-zinc-400">
<span>{{ item.period }}</span>
<div class="flex flex-wrap gap-1">
@for (tag of item.tags; track tag) {
<span
class="rounded-full bg-zinc-100 px-2 py-1 text-[11px] text-zinc-700 dark:bg-slate-800/80 dark:text-zinc-200"
>
{{ tag }}
</span>
}
</div>
</div>
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-50">
{{ item.title }}
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-300">
{{ item.summary }}
</p>
</article>
}
</div>
</app-section>
`
})
export class PortfolioSectionComponent {
readonly sectionId = 'portfolio';
readonly items: PortfolioItem[] = [
{
title: 'Design system foundation',
period: '2024',
summary:
'Built a lightweight component system with clear tokens and documentation, reducing UI delivery time while keeping accessibility first.',
tags: ['Angular', 'Tailwind', 'Design systems']
},
{
title: 'Modernize a legacy SPA',
period: '2023',
summary:
'Incrementally refactored a legacy front end into modular features with typed APIs, improving stability and developer velocity.',
tags: ['Refactoring', 'TypeScript', 'DX']
},
{
title: 'API-first product slice',
period: '2022',
summary:
'Shipped a new product slice with clear contracts between front end and services, enabling parallel delivery across teams.',
tags: ['Architecture', 'APIs', 'Collaboration']
},
{
title: 'Performance and observability pass',
period: '2021',
summary:
'Improved performance budgets, added monitoring, and set up alerting to catch regressions before they reached users.',
tags: ['Performance', 'Monitoring', 'Quality']
}
];
}

View File

@@ -0,0 +1,9 @@
import { SectionNavItem } from '../models/section-nav.model';
export const MAIN_NAVIGATION: SectionNavItem[] = [
{ label: 'Home', path: '/', fragment: 'home' },
{ label: 'About me', path: '/', fragment: 'about-me' },
{ label: 'About IT', path: '/', fragment: 'about-it' },
{ label: 'CV', path: '/cv' },
{ label: 'Portfolio', path: '/portfolio' }
];

View File

@@ -0,0 +1,5 @@
export interface SectionNavItem {
label: string;
path?: string;
fragment?: string;
}

View File

@@ -0,0 +1,46 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-section',
standalone: true,
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section
[id]="id"
class="scroll-mt-24 border-b border-zinc-200 last:border-b-0 dark:border-zinc-800/30"
>
<div
class="mx-auto flex max-w-5xl flex-col gap-6 px-4 py-16 md:px-8 md:py-24"
>
<div class="flex flex-col gap-3">
@if (eyebrow) {
<p
class="text-xs font-semibold uppercase tracking-[0.2em] text-zinc-500 dark:text-zinc-400"
>
{{ eyebrow }}
</p>
}
<div class="flex flex-col gap-3">
<h2 class="text-3xl font-semibold tracking-tight md:text-4xl">
{{ title }}
</h2>
@if (lead) {
<p class="max-w-3xl text-lg text-zinc-600 dark:text-zinc-300">
{{ lead }}
</p>
}
</div>
</div>
<ng-content />
</div>
</section>
`
})
export class SectionComponent {
@Input({ required: true }) id!: string;
@Input({ required: true }) title!: string;
@Input() eyebrow?: string;
@Input() lead?: string;
}

View File

@@ -0,0 +1,68 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ThemeService, Theme } from '../../../core/services/theme.service';
@Component({
selector: 'app-theme-toggle',
standalone: true,
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="relative flex items-center rounded-full border border-zinc-200 bg-white/50 p-1 backdrop-blur dark:border-slate-800 dark:bg-slate-950/50">
<button
type="button"
class="relative flex h-7 w-7 items-center justify-center rounded-full text-zinc-500 transition hover:text-zinc-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 dark:text-slate-400 dark:hover:text-slate-100"
[class.bg-white]="themeService.theme() === 'light'"
[class.text-emerald-600]="themeService.theme() === 'light'"
[class.shadow-sm]="themeService.theme() === 'light'"
[class.dark:bg-slate-800]="themeService.theme() === 'light'"
[class.dark:text-emerald-400]="themeService.theme() === 'light'"
(click)="setTheme('light')"
aria-label="Light theme"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd" />
</svg>
</button>
<button
type="button"
class="relative flex h-7 w-7 items-center justify-center rounded-full text-zinc-500 transition hover:text-zinc-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 dark:text-slate-400 dark:hover:text-slate-100"
[class.bg-white]="themeService.theme() === 'system'"
[class.text-emerald-600]="themeService.theme() === 'system'"
[class.shadow-sm]="themeService.theme() === 'system'"
[class.dark:bg-slate-800]="themeService.theme() === 'system'"
[class.dark:text-emerald-400]="themeService.theme() === 'system'"
(click)="setTheme('system')"
aria-label="System theme"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
<path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm2 0v8h10V5H5z" clip-rule="evenodd" />
</svg>
</button>
<button
type="button"
class="relative flex h-7 w-7 items-center justify-center rounded-full text-zinc-500 transition hover:text-zinc-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 dark:text-slate-400 dark:hover:text-slate-100"
[class.bg-white]="themeService.theme() === 'dark'"
[class.text-emerald-600]="themeService.theme() === 'dark'"
[class.shadow-sm]="themeService.theme() === 'dark'"
[class.dark:bg-slate-800]="themeService.theme() === 'dark'"
[class.dark:text-emerald-400]="themeService.theme() === 'dark'"
(click)="setTheme('dark')"
aria-label="Dark theme"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
</button>
</div>
`
})
export class ThemeToggleComponent {
readonly themeService = inject(ThemeService);
setTheme(theme: Theme) {
this.themeService.setTheme(theme);
}
}

13
src/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>SWE - Amar Džanan</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="duck.png">
</head>
<body>
<app-root></app-root>
</body>
</html>

8
src/main.server.ts Normal file
View File

@@ -0,0 +1,8 @@
import { provideZoneChangeDetection, provideZonelessChangeDetection } from "@angular/core";
import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser';
import { App } from './app/app';
import { config } from './app/app.config.server';
const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, {...config, providers: [provideZonelessChangeDetection(), ...config.providers]}, context);
export default bootstrap;

6
src/main.ts Normal file
View File

@@ -0,0 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));

68
src/server.ts Normal file
View File

@@ -0,0 +1,68 @@
import {
AngularNodeAppEngine,
createNodeRequestHandler,
isMainModule,
writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { join } from 'node:path';
const browserDistFolder = join(import.meta.dirname, '../browser');
const app = express();
const angularApp = new AngularNodeAppEngine();
/**
* Example Express Rest API endpoints can be defined here.
* Uncomment and define endpoints as necessary.
*
* Example:
* ```ts
* app.get('/api/{*splat}', (req, res) => {
* // Handle API request
* });
* ```
*/
/**
* Serve static files from /browser
*/
app.use(
express.static(browserDistFolder, {
maxAge: '1y',
index: false,
redirect: false,
}),
);
/**
* Handle all other requests by rendering the Angular application.
*/
app.use((req, res, next) => {
angularApp
.handle(req)
.then((response) =>
response ? writeResponseToNodeResponse(response, res) : next(),
)
.catch(next);
});
/**
* Start the server if this module is the main entry point.
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
*/
if (isMainModule(import.meta.url)) {
const port = process.env['PORT'] || 4000;
app.listen(port, (error) => {
if (error) {
throw error;
}
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
/**
* Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
*/
export const reqHandler = createNodeRequestHandler(app);

54
src/styles.scss Normal file
View File

@@ -0,0 +1,54 @@
@use 'tailwindcss';
@theme {
--font-saira: 'Saira', sans-serif;
}
@font-face {
font-family: 'Saira';
src: url('/fonts/Saira-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Saira';
src: url('/fonts/Saira-Italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Saira';
src: url('/fonts/Saira-SemiBold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Saira';
src: url('/fonts/Saira-SemiBoldItalic.ttf') format('truetype');
font-weight: 600;
font-style: italic;
font-display: swap;
}
@variant dark (&:where(.dark, .dark *));
@keyframes fade-in-up {
0% {
opacity: 0;
transform: translateY(12px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
html {
scroll-behavior: smooth;
}

17
tsconfig.app.json Normal file
View File

@@ -0,0 +1,17 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
"node"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
]
}

36
tsconfig.json Normal file
View File

@@ -0,0 +1,36 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"esModuleInterop": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"moduleResolution": "bundler",
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

14
tsconfig.spec.json Normal file
View File

@@ -0,0 +1,14 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.ts"
]
}