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