This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
.angular
|
||||
.git
|
||||
.gitea
|
||||
npm-debug.log
|
||||
coverage
|
||||
17
.editorconfig
Normal file
17
.editorconfig
Normal 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
|
||||
61
.gitea/workflows/deploy.yml
Normal file
61
.gitea/workflows/deploy.yml
Normal 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
42
.gitignore
vendored
Normal 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
5
.postcssrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
}
|
||||
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal 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
20
.vscode/launch.json
vendored
Normal 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
42
.vscode/tasks.json
vendored
Normal 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
22
Dockerfile
Normal 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
59
README.md
Normal 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
96
angular.json
Normal 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
11473
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
package.json
Normal file
56
package.json
Normal 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
BIN
public/duck.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/fonts/Saira-Italic.ttf
Normal file
BIN
public/fonts/Saira-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Saira-Regular.ttf
Normal file
BIN
public/fonts/Saira-Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Saira-SemiBold.ttf
Normal file
BIN
public/fonts/Saira-SemiBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Saira-SemiBoldItalic.ttf
Normal file
BIN
public/fonts/Saira-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/images/profile.jpg
Normal file
BIN
public/images/profile.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
public/images/timeline.webp
Normal file
BIN
public/images/timeline.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
12
src/app/app.config.server.ts
Normal file
12
src/app/app.config.server.ts
Normal 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
19
src/app/app.config.ts
Normal 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
1
src/app/app.html
Normal file
@@ -0,0 +1 @@
|
||||
<router-outlet />
|
||||
8
src/app/app.routes.server.ts
Normal file
8
src/app/app.routes.server.ts
Normal 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
17
src/app/app.routes.ts
Normal 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
14
src/app/app.ts
Normal 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);
|
||||
}
|
||||
99
src/app/core/layout/page-shell.component.ts
Normal file
99
src/app/core/layout/page-shell.component.ts
Normal 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[] = [];
|
||||
}
|
||||
59
src/app/core/services/theme.service.ts
Normal file
59
src/app/core/services/theme.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
85
src/app/features/about/about-it-section.component.ts
Normal file
85
src/app/features/about/about-it-section.component.ts
Normal 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';
|
||||
}
|
||||
60
src/app/features/about/about-me-section.component.ts
Normal file
60
src/app/features/about/about-me-section.component.ts
Normal 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';
|
||||
}
|
||||
19
src/app/features/cv/cv-page.component.ts
Normal file
19
src/app/features/cv/cv-page.component.ts
Normal 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;
|
||||
}
|
||||
80
src/app/features/cv/cv-section.component.ts
Normal file
80
src/app/features/cv/cv-section.component.ts
Normal 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';
|
||||
}
|
||||
146
src/app/features/home/home-section.component.ts
Normal file
146
src/app/features/home/home-section.component.ts
Normal 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="Here’s 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 }} • {{ 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 }} • {{ 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 }} • {{ 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
|
||||
}
|
||||
];
|
||||
}
|
||||
28
src/app/features/landing/landing-page.component.ts
Normal file
28
src/app/features/landing/landing-page.component.ts
Normal 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;
|
||||
}
|
||||
19
src/app/features/portfolio/portfolio-page.component.ts
Normal file
19
src/app/features/portfolio/portfolio-page.component.ts
Normal 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;
|
||||
}
|
||||
86
src/app/features/portfolio/portfolio-section.component.ts
Normal file
86
src/app/features/portfolio/portfolio-section.component.ts
Normal 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']
|
||||
}
|
||||
];
|
||||
}
|
||||
9
src/app/shared/config/navigation.config.ts
Normal file
9
src/app/shared/config/navigation.config.ts
Normal 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' }
|
||||
];
|
||||
5
src/app/shared/models/section-nav.model.ts
Normal file
5
src/app/shared/models/section-nav.model.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface SectionNavItem {
|
||||
label: string;
|
||||
path?: string;
|
||||
fragment?: string;
|
||||
}
|
||||
46
src/app/shared/ui/section/section.component.ts
Normal file
46
src/app/shared/ui/section/section.component.ts
Normal 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;
|
||||
}
|
||||
68
src/app/shared/ui/theme-toggle/theme-toggle.component.ts
Normal file
68
src/app/shared/ui/theme-toggle/theme-toggle.component.ts
Normal 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
13
src/index.html
Normal 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
8
src/main.server.ts
Normal 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
6
src/main.ts
Normal 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
68
src/server.ts
Normal 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
54
src/styles.scss
Normal 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
17
tsconfig.app.json
Normal 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
36
tsconfig.json
Normal 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
14
tsconfig.spec.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user