first commit

This commit is contained in:
Lucio Lelii 2026-01-07 16:07:33 +01:00
parent 55892d644b
commit bab3a9797d
23 changed files with 810 additions and 238 deletions

View File

@ -22,9 +22,16 @@
{
"glob": "**/*",
"input": "public"
},
{
"glob": "**/*",
"input": "src/assets",
"output": "/assets/"
}
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/bootstrap-icons/font/bootstrap-icons.css",
"src/styles.css"
]
},

13
components.json Normal file
View File

@ -0,0 +1,13 @@
{
"style": "css",
"packageManager": "npm",
"tailwind": {
"css": "src/styles.css",
"baseColor": "stone",
"cssVariables": true
},
"aliases": {
"components": "src/app/shared/components",
"utils": "src/app/shared/utils"
}
}

428
package-lock.json generated
View File

@ -9,25 +9,32 @@
"version": "0.0.0",
"dependencies": {
"@angular/animations": "^21.0.4",
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0",
"@angular/cdk": "^21.0.3",
"@angular/common": "^21.0.4",
"@angular/compiler": "^21.0.4",
"@angular/core": "^21.0.4",
"@angular/forms": "^21.0.4",
"@angular/platform-browser": "^21.0.4",
"@angular/platform-browser-dynamic": "^21.0.4",
"@angular/router": "^21.0.0",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-angular": "^0.561.0",
"rxjs": "~7.8.0",
"tailwind-merge": "^3.4.0",
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular/build": "^21.0.3",
"@angular/cli": "^21.0.3",
"@angular/compiler-cli": "^21.0.0",
"@tailwindcss/postcss": "^4.1.12",
"@tailwindcss/postcss": "^4.1.18",
"jsdom": "^27.1.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.12",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"tailwindcss-animate": "^1.0.7",
"typescript": "~5.9.2",
"vitest": "^4.0.8"
}
@ -339,9 +346,9 @@
}
},
"node_modules/@angular/animations": {
"version": "21.0.4",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.0.4.tgz",
"integrity": "sha512-SIOFpdNuueqhw1NvpLU9pD1lNaHAtLbrTbdAD3ALbutylHZwjFTnzuiU+2mHZZa9NvAnXD6fFc/jhbNU2Ehm3g==",
"version": "21.0.5",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.0.5.tgz",
"integrity": "sha512-7Lr60wLlYcGG+VDnnOY9xpn8Zz3yyJcWGSjNEbXPEGaaD0nTZLNZ1nIXRhTeYZwosK5GvPDFxq68kdLxczskHA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -350,7 +357,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/core": "21.0.4"
"@angular/core": "21.0.5"
}
},
"node_modules/@angular/build": {
@ -453,6 +460,21 @@
}
}
},
"node_modules/@angular/cdk": {
"version": "21.0.3",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.0.3.tgz",
"integrity": "sha512-abfckeZfFvovdpxuQHRE4gS1VLNa05Dx0ZSKLGVL9DsQsi4pgn6wWg1y9TkXMlmtpG/EhLmCBxUc6LOHfdeWQA==",
"license": "MIT",
"dependencies": {
"parse5": "^8.0.0",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "^21.0.0 || ^22.0.0",
"@angular/core": "^21.0.0 || ^22.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/cli": {
"version": "21.0.3",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.3.tgz",
@ -490,9 +512,9 @@
}
},
"node_modules/@angular/common": {
"version": "21.0.4",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.4.tgz",
"integrity": "sha512-hBEq0d01UNPgngxHApg4sVaX9y9xAV5BZnAnlVW0ph2uECZ03X7ortKwFDXeWMFd+0oPOLNqxr3WkzITPZ1d0g==",
"version": "21.0.5",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.5.tgz",
"integrity": "sha512-/ZI11F6Wxr8TZRVO4O7pmhBJ9YxDg9mvA76e0PiivmqZggM02HY0y3XPMP3hAOe4K+PfaVBgMAu3P9t32klzfA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -501,14 +523,14 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/core": "21.0.4",
"@angular/core": "21.0.5",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/compiler": {
"version": "21.0.4",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.4.tgz",
"integrity": "sha512-I5Y1BjizOv5gTiix3+iRycjQtXgUECJTgacTZw8wZmu2T1Qv0BMRqTeXZ87iaD6EUNxFLwneJq3AOrZe3imtKw==",
"version": "21.0.5",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.5.tgz",
"integrity": "sha512-92sv9pVm9o/8KfPM7T8j5VQmTaSOqmIajrJF8evXE2dNJcwkBpVtzZUqDzr23AV3vg94C7eYU64i8qrsmJ+cYQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -518,9 +540,9 @@
}
},
"node_modules/@angular/compiler-cli": {
"version": "21.0.4",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.0.4.tgz",
"integrity": "sha512-CZeTia783iQ2Pp8ulWa2itTKC4YW7gBM0H4MDc4wbKlN2NSNzzUlttL5sQ4lKKq8DUZk3Cmj3DfJbwsECtQe5Q==",
"version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.0.6.tgz",
"integrity": "sha512-UcIUx+fbn0VLlCBCIYxntAzWG3zPRUo0K7wvuK0MC6ZFCWawgewx9SdLLZTqcaWe1g5FRQlQeVQcFgHAO5R2Mw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -541,7 +563,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/compiler": "21.0.4",
"@angular/compiler": "21.0.6",
"typescript": ">=5.9 <6.0"
},
"peerDependenciesMeta": {
@ -551,9 +573,9 @@
}
},
"node_modules/@angular/core": {
"version": "21.0.4",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.4.tgz",
"integrity": "sha512-VGFTvPdG5eowHi+BSYHRIvaFJuxhMejgulNKVz+ewDcq/U0KkNtEYFL2w0sNz4jBMrC4YsdRU/PwIpL/ATHryg==",
"version": "21.0.5",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.5.tgz",
"integrity": "sha512-HFXfO5YsBVM+IEaU8h3DZSxO98yDZM2v49NlSVNDzFD3fhnkpTmcgT2NKz9ulIiuV9N376itt+x+NG12sg/+Fw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -562,7 +584,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/compiler": "21.0.4",
"@angular/compiler": "21.0.5",
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.15.0 || ~0.16.0"
},
@ -576,9 +598,9 @@
}
},
"node_modules/@angular/forms": {
"version": "21.0.4",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.4.tgz",
"integrity": "sha512-xWU5TN1TINX0gdIApEG5YVdHQwBzVwRSNr3Wqx/nOkOvmUZlFCcgnpvO5hj50wlp2ASvZX3EsRNJQr6kV0F8bw==",
"version": "21.0.5",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.5.tgz",
"integrity": "sha512-RcmXs/LgKyc7D70xVT+3aK/H2SCFEyuebAiw72Iz1te1Gbql2GDFF6hgEOaNwOUglDg8ogN5MdVif2DbRLD3Hw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -587,17 +609,44 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/common": "21.0.4",
"@angular/core": "21.0.4",
"@angular/platform-browser": "21.0.4",
"@angular/common": "21.0.5",
"@angular/core": "21.0.5",
"@angular/platform-browser": "21.0.5",
"@standard-schema/spec": "^1.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/localize": {
"version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.0.6.tgz",
"integrity": "sha512-t4Jf4px49YAuF7G0eLxknB+KNE+tdqzhAo2w//9kjZGg/xHPp9H/RQUlNUB/RYJuabbBh4IpiRvpJZqp7rKt8A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@babel/core": "7.28.4",
"@types/babel__core": "7.20.5",
"tinyglobby": "^0.2.12",
"yargs": "^18.0.0"
},
"bin": {
"localize-extract": "tools/bundles/src/extract/cli.js",
"localize-migrate": "tools/bundles/src/migrate/cli.js",
"localize-translate": "tools/bundles/src/translate/cli.js"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/compiler": "21.0.6",
"@angular/compiler-cli": "21.0.6"
}
},
"node_modules/@angular/platform-browser": {
"version": "21.0.4",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.4.tgz",
"integrity": "sha512-f2gYavjKrf60JYkBt6G2M4bCLZGnh3ZXf7OOpBzCladdzxoC/z7RFhtJ+3q5pFUkhRlgD+zgNtdW5dDDoplUbg==",
"version": "21.0.5",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.5.tgz",
"integrity": "sha512-UVCrqOxFmX6kAG3Y6jqjCWvLoTP7fxeY96AsxTMp1fkBdqbQbEPleWQpwngNimsuUPvf+rA6XOxsqiDmRex5mA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -606,9 +655,9 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/animations": "21.0.4",
"@angular/common": "21.0.4",
"@angular/core": "21.0.4"
"@angular/animations": "21.0.5",
"@angular/common": "21.0.5",
"@angular/core": "21.0.5"
},
"peerDependenciesMeta": {
"@angular/animations": {
@ -617,9 +666,9 @@
}
},
"node_modules/@angular/platform-browser-dynamic": {
"version": "21.0.4",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.0.4.tgz",
"integrity": "sha512-smFTnPdEWvhYAGFkFDUuSDaJ5OSGSlSpabo0RQm+iUJYILLJ4/pImRu4P9ebS8CkZ+boNS1CZJIy0DK75Mq20Q==",
"version": "21.0.5",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.0.5.tgz",
"integrity": "sha512-0P5vFSS6UhiU7IBeVqPEKmRhMtyQqyXGN9+zF7kLK8H0cx1j0eGVmHRsVuY2YKoVp97fXDIeVGSbO0t5ZcFhoA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -628,16 +677,16 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/common": "21.0.4",
"@angular/compiler": "21.0.4",
"@angular/core": "21.0.4",
"@angular/platform-browser": "21.0.4"
"@angular/common": "21.0.5",
"@angular/compiler": "21.0.5",
"@angular/core": "21.0.5",
"@angular/platform-browser": "21.0.5"
}
},
"node_modules/@angular/router": {
"version": "21.0.4",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.4.tgz",
"integrity": "sha512-lDLOhr/8Egb5WqgWTY/fz3SbVgU4OFjIDlz7S49uU+AH33Q6jtA6J6cv7ilZ2cBupikv3jVIa5PjdW4c82+y7g==",
"version": "21.0.5",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.5.tgz",
"integrity": "sha512-IFmf0Wd7jSOoZ8TI+4RXMsYmnIfHQG+kGxeMQVKrefTdr3uEHW/TEsNzbW5bkCpVJHRm4EhkH4hSu8D8tUQffQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -646,9 +695,9 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/common": "21.0.4",
"@angular/core": "21.0.4",
"@angular/platform-browser": "21.0.4",
"@angular/common": "21.0.5",
"@angular/core": "21.0.5",
"@angular/platform-browser": "21.0.5",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
@ -3944,9 +3993,9 @@
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
"integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3956,7 +4005,7 @@
"lightningcss": "1.30.2",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.17"
"tailwindcss": "4.1.18"
}
},
"node_modules/@tailwindcss/node/node_modules/magic-string": {
@ -3970,33 +4019,33 @@
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
"integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.17",
"@tailwindcss/oxide-darwin-arm64": "4.1.17",
"@tailwindcss/oxide-darwin-x64": "4.1.17",
"@tailwindcss/oxide-freebsd-x64": "4.1.17",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
"@tailwindcss/oxide-linux-x64-musl": "4.1.17",
"@tailwindcss/oxide-wasm32-wasi": "4.1.17",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
"@tailwindcss/oxide-android-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-x64": "4.1.18",
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
"integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
"cpu": [
"arm64"
],
@ -4011,9 +4060,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
"integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
"cpu": [
"arm64"
],
@ -4028,9 +4077,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
"integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
"cpu": [
"x64"
],
@ -4045,9 +4094,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
"integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
"cpu": [
"x64"
],
@ -4062,9 +4111,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
"integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
"cpu": [
"arm"
],
@ -4079,9 +4128,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
"integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
"cpu": [
"arm64"
],
@ -4096,9 +4145,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
"integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
"cpu": [
"arm64"
],
@ -4113,9 +4162,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
"integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
"cpu": [
"x64"
],
@ -4130,9 +4179,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
"integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
"cpu": [
"x64"
],
@ -4147,9 +4196,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
"integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@ -4165,10 +4214,10 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.6.0",
"@emnapi/runtime": "^1.6.0",
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.0.7",
"@napi-rs/wasm-runtime": "^1.1.0",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.4.0"
},
@ -4177,9 +4226,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
"integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
"cpu": [
"arm64"
],
@ -4194,9 +4243,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
"integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
"cpu": [
"x64"
],
@ -4211,17 +4260,17 @@
}
},
"node_modules/@tailwindcss/postcss": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz",
"integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz",
"integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.1.17",
"@tailwindcss/oxide": "4.1.17",
"@tailwindcss/node": "4.1.18",
"@tailwindcss/oxide": "4.1.18",
"postcss": "^8.4.41",
"tailwindcss": "4.1.17"
"tailwindcss": "4.1.18"
}
},
"node_modules/@tufjs/canonical-json": {
@ -4275,6 +4324,59 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
"@types/babel__generator": "*",
"@types/babel__template": "*",
"@types/babel__traverse": "*"
}
},
"node_modules/@types/babel__generator": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__template": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__traverse": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@ -4606,9 +4708,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz",
"integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==",
"version": "2.9.7",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz",
"integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -4696,6 +4798,22 @@
"@popperjs/core": "^2.11.8"
}
},
"node_modules/bootstrap-icons": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz",
"integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@ -4925,6 +5043,18 @@
"node": ">=18"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"license": "Apache-2.0",
"dependencies": {
"clsx": "^2.1.1"
},
"funding": {
"url": "https://polar.sh/cva"
}
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
@ -5032,6 +5162,15 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -5388,9 +5527,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"version": "5.18.4",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
"integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -6075,9 +6214,9 @@
}
},
"node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -6853,6 +6992,19 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-angular": {
"version": "0.561.0",
"resolved": "https://registry.npmjs.org/lucide-angular/-/lucide-angular-0.561.0.tgz",
"integrity": "sha512-/DVDEVDJM08h8bGarLH65+4jfLcjaGhHcG1TdDXvo0hX1CXczG0NzEIEs6CJdddfEdO+wzv3KLGpAz1QPw7sSg==",
"license": "ISC",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "13.x - 21.x",
"@angular/core": "13.x - 21.x"
}
},
"node_modules/magic-string": {
"version": "0.30.19",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
@ -7684,7 +7836,6 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
@ -7738,7 +7889,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@ -8650,13 +8800,33 @@
"dev": true,
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"dev": true,
"license": "MIT"
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
"integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",

View File

@ -24,25 +24,32 @@
"packageManager": "npm@10.8.2",
"dependencies": {
"@angular/animations": "^21.0.4",
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0",
"@angular/cdk": "^21.0.3",
"@angular/common": "^21.0.4",
"@angular/compiler": "^21.0.4",
"@angular/core": "^21.0.4",
"@angular/forms": "^21.0.4",
"@angular/platform-browser": "^21.0.4",
"@angular/platform-browser-dynamic": "^21.0.4",
"@angular/router": "^21.0.0",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-angular": "^0.561.0",
"rxjs": "~7.8.0",
"tailwind-merge": "^3.4.0",
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular/build": "^21.0.3",
"@angular/cli": "^21.0.3",
"@angular/compiler-cli": "^21.0.0",
"@tailwindcss/postcss": "^4.1.12",
"@tailwindcss/postcss": "^4.1.18",
"jsdom": "^27.1.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.12",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"tailwindcss-animate": "^1.0.7",
"typescript": "~5.9.2",
"vitest": "^4.0.8"
}

BIN
public/logoHumAInFlow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
public/logoNoName.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
public/user-icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,25 @@
/* Header fisso */
.app-header {
font-family: 'Roboto', sans-serif;
border-bottom: 1px solid #ddd;
}
/* Logo */
.logo {
height: 40px;
}
/* User section */
.user-section i {
font-size: 24px;
}
/* Contenuto principale */
.main-content {
min-height: 0; /* necessario per overflow-auto su flex */
}
/* Tabs sticky */
.nav-tabs {
border-bottom: 1px solid #ccc;
}

View File

@ -1 +1,43 @@
<p>app-layout works!</p>
<div class="main-layout min-h-screen flex flex-col">
<!-- Header fisso -->
<header class="app-header fixed top-0 left-0 right-0 z-50 flex justify-between items-center px-4 h-16 bg-[#d8f4e7] border-b border-gray-300">
<div class="left-section flex items-center gap-4">
<img src="/logoNoName.png" alt="Logo" class="logo h-10" />
<h1 class="app-title text-lg font-medium text-gray-800 m-0">HumAIn Flow</h1>
</div>
<div class="user-section flex items-center gap-2 text-gray-700 font-medium">
<i class="bi bi-person-circle fs-4"></i>
<span class="username">{{ loggedUser()?.username }}</span>
<button class="btn btn-sm btn-light border-0 p-1" (click)="logout()" title="Logout" aria-label="Logout">
<i class="bi bi-box-arrow-right text-secondary"></i>
</button>
</div>
</header>
<!-- Wrapper per compensare header fisso -->
<div class="flex-1 pt-16 flex flex-col">
<!-- Tabs sticky sotto header -->
<ul class="nav nav-tabs sticky-top top-16 z-40 bg-[#d8f4e7]">
<li class="nav-item">
<a class="nav-link active" href="#">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
</ul>
<!-- Contenuto principale scrollabile -->
<main class="main-content flex-1 overflow-auto p-4 bg-gray-50">
<p>Qui va il contenuto principale...</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit...</p>
<p>Puoi aggiungere tutto ciò che vuoi, scrollando rimane visibile header + tabs.</p>
</main>
</div>
</div>

View File

@ -1,4 +1,6 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { Authorization } from '@services/authorization/authorization';
@Component({
selector: 'app-app-layout',
@ -8,4 +10,14 @@ import { Component } from '@angular/core';
})
export class AppLayout {
router = inject(Router);
private authService = inject(Authorization);
loggedUser = this.authService.loggedInUser;
logout() {
this.authService.logout();
this.router.navigate(['/login']);
}
}

View File

@ -1,4 +1,9 @@
export type User = {
id: number;
username: string;
email: string;
};
};
export type UserRegistration = User & {
password: string;
fullname: string;
}

View File

@ -0,0 +1,5 @@
.leaving {
opacity: 0;
transform: translateY(20px);
transition: opacity 500ms ease-out, transform 500ms ease-out;
}

View File

@ -1,40 +1,77 @@
<div class="container-centered">
@if(error()) {
<div class="flex items-start sm:items-center p-4 mb-4 text-sm text-fg-danger-strong rounded-base bg-danger-soft"
role="alert">
<svg class="w-4 h-4 me-2 shrink-0 mt-0.5 sm:mt-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 11h2v5m-2 0h4m-2.592-8.5h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<p><span class="font-medium me-1">Error</span> {{error()}}</p>
@if (error()) {
<div class="fixed top-4 left-1/2 -translate-x-1/2 z-50
w-full max-w-md
rounded-lg border border-red-300
bg-red-100 text-red-800
shadow-md px-4 py-3
flex items-start gap-3">
<!-- Icon -->
<i class="bi bi-exclamation-triangle-fill text-red-600 mt-0.5"></i>
<!-- Message -->
<div class="flex-1 text-sm leading-relaxed">
<p class="m-0">
{{ error() }}
</p>
</div>
}
<div class="card-container">
<div class="card">
<h1 class="title">Sign In</h1>
<form class="form-group" #form="ngForm" (ngSubmit)="onSubmit(form)">
<div class="form-control">
<label class="label" for="email">Email</label>
<input id="email" type="email" name="email" class="input" ngModel placeholder="example@domain.it"
#email="ngModel" email required />
@if(isInvalid(email)) {
<p class="helper-text text-red-500!">Invalid Email</p>
}
</div>
}
@if (registeredUser()) {
<div class="fixed top-4 left-1/2 -translate-x-1/2 z-50
w-full max-w-md
rounded-lg border border-green-300
bg-green-100 text-green-800
shadow-md px-4 py-3
flex items-start gap-3">
<!-- Icon -->
<i class="bi bi-check-circle-fill text-green-600 mt-0.5"></i>
<!-- Message -->
<div class="flex-1 text-sm leading-relaxed">
<p class="m-0">
Registration successful. <b>{{ registeredUser() }}</b>, now you can log in.
</p>
</div>
</div>
}
<div class="flex justify-center items-center min-h-screen bg-base-200 p-4">
<div class="border-2 border-sky-500/50 w-full max-w-md mx-auto p-7 shadow-xl rounded-md">
<div class="flex justify-center">
<img class="size-48 " alt="" src="/logoNoName.png" />
</div>
<div class="card-body p-4">
<form (ngSubmit)="onSubmit()" novalidate>
<!-- Email -->
<div class="input-group mb-3">
<span class="input-group-text"><i class="bi bi-at"></i></span>
<input type="text" id="username" class="form-control" placeholder="Username"
[field]="loginForm.username" />
</div>
<div class="form-control">
<label class="label" for="password">Password</label>
<input id="password" type="password" name="password" class="input" autocomplete="off" ngModel
placeholder="********" #password="ngModel" required />
@if(isInvalid(password)) {
<p class="helper-text text-red-500!">Password Required</p>
}
<!-- Password -->
<div class="input-group mb-3">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="password" #password id="password" class="form-control" placeholder="Password"
[field]="loginForm.password" />
<button class="input-group-text bg-white"
(click)="password.type = password.type === 'password' ? 'text' : 'password'"
[disabled]="loginFormModel().password.length === 0"><i class="bi bi-eye"></i></button>
</div>
<button type="submit" [disabled]="form.invalid" class="button">Sign In</button>
<p class="helper-text">
Don't have an account yet?
<a routerLink="/sign-up" class="link">Sign Up</a>
</p>
<!-- Sign up -->
<div class="mb-3 text-center">
<span>
Don't have an account yet?
<a routerLink="/sign-up" class="ms-1 link-primary">Sign Up</a>
</span>
</div>
<!-- Submit -->
<button type="submit" class="btn btn-outline-primary w-100" [disabled]="loginForm().invalid()">
Login
</button>
</form>
</div>
</div>

View File

@ -1,27 +1,65 @@
import { Component, inject, signal } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { Router, RouterLink } from "@angular/router";
import { Component, computed, effect, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
import { FormUtility } from '@utilities/form-utility';
import { Authorization } from '@services/authorization/authorization';
import { Field, form, required } from '@angular/forms/signals';
@Component({
selector: 'app-login',
imports: [FormsModule, RouterLink],
imports: [FormsModule, RouterLink, Field],
templateUrl: './login.html',
styleUrl: './login.css',
styleUrls: ['./login.css'],
})
export class Login extends FormUtility {
private authService = inject(Authorization);
private router = inject(Router);
error = signal<string | null>(null);
registeredUser = signal<string | null>(null);
onSubmit(form: NgForm) {
console.log(form.value);
this.authService.login(form.value.email, form.value.password).subscribe({
error: (err) => this.error.set(err.message),
complete: () => this.router.navigate(['/']),
});;
loginFormModel = signal({
username: '',
password: ''
});
loginForm = form(this.loginFormModel, (model) => {
required(model.username, { message: 'Username is required' }),
required(model.password, { message: 'Password is required' })
});
constructor() {
super();
effect(() => {
if (this.error() != null) {
setTimeout(() => this.error.set(null), 3000);
}
});
effect(() => {
if (this.registeredUser() != null) {
setTimeout(() => this.registeredUser.set(null), 5000);
}
});
}
ngOnInit(): void {
const registered = history.state?.registered;
if (registered) {
this.registeredUser.set(registered);
this.loginFormModel.set({
username: registered,
password: ''
});
}
}
onSubmit() {
console.log(this.loginFormModel());
this.authService.login(this.loginFormModel().username, this.loginFormModel().password).subscribe({
error: (err) => this.error.set(err.message),
complete: () => this.router.navigate(['/']),
});
}
}

View File

@ -1 +1,104 @@
<p>signup works!</p>
<div class="flex justify-center items-center min-h-screen bg-base-400 p-4">
<div class="border-2 border-sky-500/50 w-3/5 min-w-100 mx-auto p-7 shadow-xl rounded-md">
<div class="flex justify-center">
<img class="size-48 " alt="" src="/logoNoName.png" />
</div>
<div class="card-body p-4">
<form (ngSubmit)="onSubmit()" class="flex flex-col gap-3">
<div class="mb-3">
<label for="fullname" class="form-label">Full Name</label>
<input id="fullname" type="text" class="form-control"
[class.is-invalid]="isInvalid(signupForm.fullname())" [field]="signupForm.fullname"
placeholder="Paolo Rossi" />
@if (isInvalid(signupForm.fullname())) {
<small class="invalid-feedback">{{ signupForm.fullname().errors()[0].message }}</small>
}
</div>
<div class="flex lg:flex-row flex-col gap-3">
<div class="mb-3 w-full">
<label for="email" class="form-label">Email</label>
<input id="email" type="email" class="form-control"
[class.is-invalid]="isInvalid(signupForm.email())" [field]="signupForm.email"
placeholder="email@example.com" />
@if (isInvalid(signupForm.email())) {
<small class="invalid-feedback">{{ signupForm.email().errors()[0].message }}</small>
}
</div>
<div class="mb-3 w-full">
<label for="username" class="form-label">Username</label>
<input id="username" type="text" class="form-control"
[class.is-invalid]="isInvalid(signupForm.username())" [field]="signupForm.username"
placeholder="Username" />
@if (isInvalid(signupForm.username())) {
<small class="invalid-feedback">{{ signupForm.username().errors()[0].message }}</small>
}
</div>
</div>
<div class="flex lg:flex-row flex-col gap-3">
<div class="mb-3 w-full">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<input id="password" type="password" class="form-control" #password
[class.is-invalid]="isInvalid(signupForm.password())" [field]="signupForm.password"
placeholder="********" />
<button class="input-group-text bg-white" type="button" tabindex="-1"
(click)="password.type = password.type === 'password' ? 'text' : 'password'"
[disabled]="signupModel().password.length === 0"><i class="bi bi-eye"></i></button>
@if (isInvalid(signupForm.password())) {
<small class="invalid-feedback">{{ signupForm.password().errors()[0].message }}</small>
}
</div>
</div>
<div class="mb-3 w-full">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<div class="input-group">
<input id="confirmPassword" type="password" class="form-control" #confirmPassword
[class.is-invalid]="!isInvalid(signupForm.password()) && isInvalid(signupForm.confirmPassword())"
[field]="signupForm.confirmPassword" placeholder="********" />
<button class="input-group-text bg-white" type="button" tabindex="-1"
(click)="confirmPassword.type = confirmPassword.type === 'password' ? 'text' : 'password'"
[disabled]="signupModel().confirmPassword.length === 0"><i
class="bi bi-eye"></i></button>
@if (!isInvalid(signupForm.password()) && isInvalid(signupForm.confirmPassword())) {
<small class="invalid-feedback">{{
signupForm.confirmPassword().errors()[0].message}}</small>
}
</div>
</div>
</div>
<div class="mb-3 text-center">
<span>
Already have an account?
<a routerLink="/sign-in" class="ms-1 link-primary">Sign In</a>
</span>
</div>
@if(error()) {
<div class="mt-2 bg-red-100 border border-red-200 text-sm text-red-800 rounded-lg p-4 dark:bg-red-800/10 dark:border-red-900 dark:text-red-500"
role="alert" tabindex="-1" aria-labelledby="hs-soft-color-danger-label">
<span id="hs-soft-color-danger-label" class="font-bold">Error</span> {{error()}}
</div>
} @else {
@if (!isRegistering()){
<!-- Submit -->
<button type="submit" class="btn btn-outline-primary w-full p-2" [disabled]="signupForm().invalid()">
Register
</button>
} @else {
<button type="button" class="btn btn-outline-primary w-full p-2" disabled>
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
Registering...
</button>
}
}
</form>
</div>
</div>
</div>

View File

@ -1,11 +1,82 @@
import { Component } from '@angular/core';
import { Component, computed, effect, ElementRef, inject, signal, ViewChild, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { form, Field, minLength, email, required, validate, maxLength, disabled } from '@angular/forms/signals'
import { Router, RouterLink } from '@angular/router';
import { UserRegistration } from '@models/user';
import { Authorization } from '@services/authorization/authorization';
import { FormUtility } from '@utilities/form-utility';
import { max, min } from 'rxjs';
@Component({
selector: 'app-signup',
imports: [],
imports: [FormsModule, Field, RouterLink],
templateUrl: './signup.html',
styleUrl: './signup.css',
})
export class Signup {
export class Signup extends FormUtility {
private authService = inject(Authorization);
private router = inject(Router);
error = signal<string | null>(null);
isRegistering = signal(false);
constructor() {
super();
effect(() => {
if (this.error() != null) {
setTimeout(() => {
this.error.set(null);
}, 3000);
}
});
}
signupModel = signal<UserRegistration & { confirmPassword: string }>({
username: '',
email: '',
password: '',
confirmPassword: '',
fullname: ''
});
signupForm = form(this.signupModel, (model) => {
required(model.username, { message: 'Username is required' }),
minLength(model.username, 4, { message: 'Username must be at least 4 characters long' }),
maxLength(model.username, 20, { message: 'Username cannot exceed 20 characters' }),
email(model.email, { message: 'Invalid email address' }),
required(model.email, { message: 'Email is required' }),
minLength(model.password, 8, { message: 'Password must be at least 8 characters long' })
required(model.fullname, { message: 'Full name is required'}),
validate(model.confirmPassword, ({value, valueOf}) => {
const confirmPassword = value()
const password = valueOf(model.password)
if (confirmPassword !== password) {
return {
kind: 'passwordMismatch',
message: 'Passwords do not match'
}
}
return null
}),
disabled(model, this.isRegistering)
});
onSubmit() {
this.isRegistering.set(true)
console.log(this.signupModel());
this.authService.signup(this.signupModel()).subscribe({
error: (err) => {
this.isRegistering.set(false);
this.error.set(err.message);
},
complete: () => {
this.isRegistering.set(false);
this.router.navigate(['/login'], { state: { registered: this.signupModel().username } });
}
});
}
}

View File

@ -1,10 +1,10 @@
import { User } from "@models/user";
import { User, UserRegistration } from "@models/user";
import { Observable } from "rxjs";
export abstract class AuthorizationCallServiceBase {
abstract login(username: string, password: string): Observable<User>;
abstract register(username: string, password: string, email: string): Observable<void>;
abstract register(userRegistration: UserRegistration): Observable<void>;
}

View File

@ -1,42 +1,48 @@
import { Observable } from "rxjs";
import { AuthorizationCallServiceBase } from "./authorization-call.base";
import { User } from "@models/user";
import { User, UserRegistration } from "@models/user";
export class AuthorizationCallFakeService extends AuthorizationCallServiceBase {
private users: User[] = [
{ id: 1, email: 'testuser@example.com' },
];
login(email: string, password: string): Observable<User> {
return new Observable<User>((observer) => {
const user = this.users.find(u => u.email === email);
if (!user || password !== 'password') {
observer.error(new Error('Invalid username or password'));
return;
}
observer.next(user);
observer.complete();
});
private users: UserRegistration[] = [
{ username: 'testuser', email: 'testuser@example.com', password: 'password123', fullname: 'Test User' },
];
login(username: string, password: string): Observable<User> {
return new Observable<User>((observer) => {
const user = this.users.find(u => u.username === username);
if (!user) {
observer.error(new Error('Invalid username'));
return;
}
if (password !== user.password) {
observer.error(new Error(`Invalid password for ${username}`));
return;
}
observer.next(user);
observer.complete();
});
}
register(username: string, password: string, email: string): Observable<void> {
return new Observable<void>((observer) => {
// Simulate a successful registration response
if (this.users.find(user => user.email === email)) {
observer.error(new Error('Email already exists'));
return;
}
this.users.push({ id: this.users.length + 1, email });
observer.next();
observer.complete();
});
}
register(userRegistration: UserRegistration): Observable<void> {
return new Observable<void>((observer) => {
setTimeout(() => {
// Simulate a successful registration response
if (this.users.find(user => user.email === userRegistration.email)) {
observer.error(new Error('Email already exists'));
return;
}
this.users.push(userRegistration);
observer.next();
observer.complete();
}, 3000);
});
}
}
}

View File

@ -1,7 +1,7 @@
import { HttpClient } from "@angular/common/http";
import { AuthorizationCallServiceBase } from "./authorization-call.base";
import { inject } from "@angular/core";
import { User } from "@models/user";
import { User, UserRegistration } from "@models/user";
import { Observable } from "rxjs";
export class AuthorizationCallService extends AuthorizationCallServiceBase {
@ -11,7 +11,7 @@ export class AuthorizationCallService extends AuthorizationCallServiceBase {
override login(username: string, password: string): Observable<User> {
throw new Error("Method not implemented.");
}
override register(username: string, password: string, email: string): Observable<void> {
override register(userRegistration: UserRegistration): Observable<void> {
throw new Error("Method not implemented.");
}

View File

@ -1,5 +1,5 @@
import { inject, Injectable, signal } from '@angular/core';
import { User } from '@models/user';
import { Injectable, signal } from '@angular/core';
import { User, UserRegistration } from '@models/user';
import { AuthorizationCallServiceBase } from './authorization-call.base';
import { environment } from '@environment';
import { take, tap } from 'rxjs';
@ -14,7 +14,9 @@ export class Authorization {
authCall: AuthorizationCallServiceBase = new environment.authorizationCallService();
user = signal<User | null>(null);
private user = signal<User | null>(null);
loggedInUser = this.user.asReadonly();
login(username: string, password: string) {
return this.authCall.login(username, password).pipe(
@ -25,6 +27,11 @@ export class Authorization {
);
}
signup(userRegistration: UserRegistration) {
return this.authCall.register(userRegistration).pipe(
take(1)
);
}
logout() {
this.user.set(null);

View File

@ -1,7 +1,11 @@
import { NgModel } from "@angular/forms";
import { FieldState } from "@angular/forms/signals";
export class FormUtility {
isInvalid(formControl: NgModel) {
return formControl.touched && formControl.dirty && formControl.invalid;
isInvalid(input: FieldState<any>) {
if (input.touched() && input.dirty() && input.invalid()) {
return true;
}
return false;
}
}

View File

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" data-theme="nord" >
<head>
<meta charset="utf-8">
<title>HumainFlowGuiA21</title>

View File

@ -5,13 +5,33 @@
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@pages/*" : ["app/pages/*"],
"@layouts/*" : ["app/layouts/*"],
"@services/*" : ["app/services/*"],
"@environment" : ["environments/environment"],
"@models/*" : ["app/models/*"],
"@utilities/*" : ["app/utilities/*"],
"@guards/*" : ["app/guards/*"]
"@pages/*": [
"app/pages/*"
],
"@layouts/*": [
"app/layouts/*"
],
"@services/*": [
"app/services/*"
],
"@environment": [
"environments/environment"
],
"@models/*": [
"app/models/*"
],
"@utilities/*": [
"app/utilities/*"
],
"@guards/*": [
"app/guards/*"
],
"@shared/*": [
"app/shared/*"
],
"@assets/*": [
"/assets/*"
]
},
"strict": true,
"noImplicitOverride": true,
@ -40,4 +60,4 @@
"path": "./tsconfig.spec.json"
}
]
}
}