Compare commits
97 Commits
f588461be1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 79ce5eadbb | |||
| 82dd3c607a | |||
| f89a27320f | |||
| 564e735b14 | |||
| ec5055e8d6 | |||
| 5050da9936 | |||
| d25b7cb49c | |||
| 9b3e42a161 | |||
| 96e3854945 | |||
| bab5f6fd15 | |||
| 0e8fe172b5 | |||
| 105514423f | |||
| 5d6a11e253 | |||
| 2d4805228a | |||
| 63679b5087 | |||
| eef3dbc2fb | |||
| 816252308c | |||
| 36df70bf38 | |||
| 91c09a0ca3 | |||
| 0d49ee6dd8 | |||
| 3c87de3d51 | |||
| 736a658323 | |||
| f140ef403b | |||
| 13b19d5776 | |||
| 4e53684649 | |||
| 836c6652ec | |||
| 7de993a765 | |||
| 6babbea1c4 | |||
| 5d4c0c5d36 | |||
| 3ee45adaa6 | |||
| 267f660512 | |||
| 454f93fb11 | |||
| e7c65dd268 | |||
| 73bf96f1ef | |||
| e827a14284 | |||
| 1146bf6f8d | |||
| d4723e6a24 | |||
| b07bc1db30 | |||
| 010646da8a | |||
| 9b62766c66 | |||
| 2c172dd3d1 | |||
| e803c670f4 | |||
| c0a3da4635 | |||
| 2c332c6758 | |||
| ea2779c681 | |||
| f89fe2e323 | |||
| 1502418cfa | |||
| 1bf0b71ca7 | |||
| d44ba2b421 | |||
| f06d5396ad | |||
| 2e77ab3e40 | |||
| 9a3dd87896 | |||
| 2e6d0c1f0a | |||
| 6615912a35 | |||
| 1723ea7a39 | |||
| ae249ea828 | |||
| 4397e5fec3 | |||
| f34ffec5b0 | |||
| a4aba009b8 | |||
| 7208a08ffb | |||
| 85da313957 | |||
| 3774750a56 | |||
| 68866f4725 | |||
| 00209ce137 | |||
| 490b929fa9 | |||
| 94eb9b1bf8 | |||
| c5e6ccc4c8 | |||
| 9c6750617b | |||
| 3f4edb954c | |||
| dfacc39e57 | |||
| e125fb6e64 | |||
| 0a56fc6be0 | |||
| 3467a574c1 | |||
| adc20904cc | |||
| af84abe3f8 | |||
| 4c51822bf0 | |||
| 8e53710c90 | |||
| 4655ba4f64 | |||
| ead327d44f | |||
| a8ff28829d | |||
| 888d636be6 | |||
| 230ba670ab | |||
| fd763cc162 | |||
| 8b462f98f6 | |||
| 90233c7208 | |||
| ab2aca1d97 | |||
| fcee6e0b19 | |||
| 5e1e453b0b | |||
| a870e875c8 | |||
| 49b9389532 | |||
| 7c6045f1b3 | |||
| 7952e5632a | |||
| 92e426b316 | |||
| f095efd9e0 | |||
| 1290130d8e | |||
| 7be44bd8f2 | |||
| 0938dc861d |
31
angular.json
@@ -27,6 +27,16 @@
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.js",
|
||||
"input": "./node_modules/angular-web-sqlite/src/lib/assets",
|
||||
"output": "./sqlite-client/"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "./node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/",
|
||||
"output": "./sqlite-client/"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
@@ -38,8 +48,8 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
"maximumWarning": "2MB",
|
||||
"maximumError": "4MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
@@ -47,7 +57,8 @@
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
"outputHashing": "all",
|
||||
"serviceWorker": "ngsw-config.json"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
@@ -67,10 +78,20 @@
|
||||
"buildTarget": "groceries-price-tracker:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"headers": {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:unit-test"
|
||||
"builder": "@angular/build:unit-test",
|
||||
"options": {
|
||||
"runnerConfig": "vitest-base.config.ts",
|
||||
"coverageExclude": ["**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
cert/server.crt
Normal file
@@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIESzCCAzOgAwIBAgIUeHJl5gcLEDqZCFSNISmGK1L7HQswDQYJKoZIhvcNAQEL
|
||||
BQAwgbQxCzAJBgNVBAYTAkFSMRUwEwYDVQQIDAxCdWVub3MgQWlyZXMxFTATBgNV
|
||||
BAcMDEJ1ZW5vcyBBaXJlczETMBEGA1UECgwKR2FiaWxhbmRpYTEeMBwGA1UECwwV
|
||||
R2FiaWxhbmRpYSBkZXZlbG9wZXJzMRMwEQYDVQQDDApHYWJpbGFuZGlhMS0wKwYJ
|
||||
KoZIhvcNAQkBFh5nYWJyaWVsLmRlbG9zcmlvc0B0dXRhbWFpbC5jb20wHhcNMjUx
|
||||
MjI3MjMyOTM1WhcNMjYxMjI3MjMyOTM1WjCBtDELMAkGA1UEBhMCQVIxFTATBgNV
|
||||
BAgMDEJ1ZW5vcyBBaXJlczEVMBMGA1UEBwwMQnVlbm9zIEFpcmVzMRMwEQYDVQQK
|
||||
DApHYWJpbGFuZGlhMR4wHAYDVQQLDBVHYWJpbGFuZGlhIGRldmVsb3BlcnMxEzAR
|
||||
BgNVBAMMCkdhYmlsYW5kaWExLTArBgkqhkiG9w0BCQEWHmdhYnJpZWwuZGVsb3Ny
|
||||
aW9zQHR1dGFtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||
AJzeRKX60Aidx2f9Vej9JagIG3B3dPMt5igVCbw8rra6k5/kUH0Vyk4TLfI9UBLX
|
||||
7T3i0c3pYZPaBm1dimNRkZEKY8QauwGfGuSR1hhyr4vwhhLaAXqnhm7CZ5fwN+PA
|
||||
WySfEqXDuyeURSLvaHdNztLFMaDnA1C0cq0waQhJTVe1xMmcFkW76qRWEkKb30oq
|
||||
FGF5GNYKVXfrzWYD4ZxW98tGrz6AhgR6XkF+aSmotKoJn6mUWQnmsnjKmBRXcaqx
|
||||
j6lJgcpdxWh8FitBB2fLUOnOZ9avcDZv9wVvVZehUgcHiAOvSIAvANdS3bKFrQcc
|
||||
6ZV/WEI0ZRQTqZ8hXufXW+kCAwEAAaNTMFEwHQYDVR0OBBYEFDyTMsJj8YEoJcAe
|
||||
FJ1LzpuV596+MB8GA1UdIwQYMBaAFDyTMsJj8YEoJcAeFJ1LzpuV596+MA8GA1Ud
|
||||
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI0UY4KLPzlc1y6VR6flFsKA
|
||||
trTBdkyICoXf2rSnRuSNS9c9cQN6j9MC3D3oymw1e3NftCh0a/Mem5A24hwK0CPQ
|
||||
ngGoGJZrfj5QHAoJ2pmIrlsmBTL66ZkvRYll/kwKHUZ3ePjaQp+JafAuhxABFhaG
|
||||
geC+c9gzSe3cIPuU6dd14AyFJewRu+VqY4fKIoHl2kwvdxzTe5/Fua6b084dbZMs
|
||||
GaEfYMkp/BuSLZVpbkliE6qq2w9qYW9yMEp9DA7nsQW+bauayTgcVWswJCzCZop2
|
||||
zaoVfNI3cyUIFDQxFNmr8uyZc9bQ+boeZ9NB1yKIWE+0RP4LrznuirGtCpHZiuo=
|
||||
-----END CERTIFICATE-----
|
||||
28
cert/server.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCc3kSl+tAIncdn
|
||||
/VXo/SWoCBtwd3TzLeYoFQm8PK62upOf5FB9FcpOEy3yPVAS1+094tHN6WGT2gZt
|
||||
XYpjUZGRCmPEGrsBnxrkkdYYcq+L8IYS2gF6p4ZuwmeX8DfjwFsknxKlw7snlEUi
|
||||
72h3Tc7SxTGg5wNQtHKtMGkISU1XtcTJnBZFu+qkVhJCm99KKhRheRjWClV3681m
|
||||
A+GcVvfLRq8+gIYEel5BfmkpqLSqCZ+plFkJ5rJ4ypgUV3GqsY+pSYHKXcVofBYr
|
||||
QQdny1DpzmfWr3A2b/cFb1WXoVIHB4gDr0iALwDXUt2yha0HHOmVf1hCNGUUE6mf
|
||||
IV7n11vpAgMBAAECggEABkNhnkxAriIOlM+qnx4sY2R0zq1rqqH0hI6Cev+Ol5Z6
|
||||
JRzVPIlNhdyCLcqA6J+ycJFz1bLccIN4qjkyjftIbUVq7P4xj+5YVolJcQ8d2d/a
|
||||
mo9rFl3woiTPmg4UooVK86VeSzwLZu3RQi8o4/U5WB+uIZctAgs9C5PZPbl02xbu
|
||||
KYO6ByNB/fksELlJGrw+46hjDxg53a6TPOtDgqGlyIweJIS3LyznvcP1qEYoXP90
|
||||
kgFu141hC4HRYprYTXykuUx8e5y/QmOQdM6of6vkT2SVFyN4YYupUsKJpRrHNDJM
|
||||
WcjXVVLs8vYhHiDJ41MnV4zoHUNgc78fCn/RE2l0AQKBgQDLGW5C5vEsC4cAQWVR
|
||||
ppCED5MtHblEH153MxXyNnnrIzCsjVCL3HQtsCcxGFxXyTMCEqpc1HdwYsMAn/PJ
|
||||
vx8W06VdHIoUkKUDf+nvVvlSdiUBWZONKOJDG06d10YdVOWr1xB7UyRMz+KE6yxe
|
||||
svIYDs53TQckH6fGx8ADAsUPwQKBgQDFuitC8obNWOgtaJb4NhbDZh9xSDQDyd3M
|
||||
CoHiTGDjviQYrN0sUs05w4bB/b/gH8vUsL3xJ6ajo1k6iSxCEyKYFPwVzG00nNz9
|
||||
oZQD0HjmtyU2Ht9Gz1OidXUkUT1/zXG0YVWTw3xcL1CkBVHeddnPwu6+LWItaI9b
|
||||
BOMhAj1WKQKBgF0doLbdqQ73jgKo+Onxgup5NZIGwa0g8K+X5WTyYv1SWfuSoq4s
|
||||
+bsEu0NAFv5Mia1Wn3MGGmiVbzA3JY+Gp5tQl81Ty17YXj93guqvpomPDzJKPmMG
|
||||
ro3z1Bx72XKTPOWHKdBQ8yCgYwtrwyD7zBEJoNGDqFWAieySIk9/EphBAoGAHnT5
|
||||
2vsetpzeTrhQoPU79mdRqpJzqK28o4Ru01vuhMYyKzbdbslmYWJz7IfYuX5MWHvN
|
||||
FkuESKqNwQ2GKrtl1cVNu1Hc9IDBLbRo51mCdg96BOcmf3LKMDkljS2SapBL1nwz
|
||||
wWoYSt7i7hD/tmOy5GTjf5ngCJyVkHClR91fc1kCgYAvlB1BodeZApt1dFjLEz8F
|
||||
l9/VzBZrMNf1Py5rHl2wFT+xM/4gXArjvKkBSCO9dqRbvmfvFTBVuI+nd2a0CoVH
|
||||
dtfc/4GzGNHzjcocScZG+UIDMLvDXpPZicEGcc2IERjlopDH/YuBlYsBd5n+19zK
|
||||
ZA5FJdXg9HW/iJMhuLyh6A==
|
||||
-----END PRIVATE KEY-----
|
||||
30
ngsw-config.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "app",
|
||||
"installMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/favicon.ico",
|
||||
"/index.csr.html",
|
||||
"/index.html",
|
||||
"/manifest.webmanifest",
|
||||
"/*.css",
|
||||
"/*.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assets",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
565
package-lock.json
generated
@@ -8,12 +8,18 @@
|
||||
"name": "groceries-price-tracker",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^21.1.0",
|
||||
"@angular/common": "^21.0.0",
|
||||
"@angular/compiler": "^21.0.0",
|
||||
"@angular/core": "^21.0.0",
|
||||
"@angular/forms": "^21.0.0",
|
||||
"@angular/material": "^21.1.0",
|
||||
"@angular/platform-browser": "^21.0.0",
|
||||
"@angular/router": "^21.0.0",
|
||||
"@angular/service-worker": "^21.0.6",
|
||||
"@ngx-translate/core": "^17.0.0",
|
||||
"@ngx-translate/http-loader": "^17.0.0",
|
||||
"angular-web-sqlite": "^1.0.34",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -21,6 +27,8 @@
|
||||
"@angular/build": "^21.0.4",
|
||||
"@angular/cli": "^21.0.4",
|
||||
"@angular/compiler-cli": "^21.0.0",
|
||||
"@vitest/browser-playwright": "^4.0.17",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"jsdom": "^27.1.0",
|
||||
"typescript": "~5.9.2",
|
||||
"vitest": "^4.0.8"
|
||||
@@ -419,6 +427,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cdk": {
|
||||
"version": "21.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.1.0.tgz",
|
||||
"integrity": "sha512-zvV37HPKhtu0bOfuK0IhjKKq++Xb57Z11uZYZJI34BZnZ5y1TPhJpcmrHhjb2uKUNfDvePUqhlnIlKAXHSBIhw==",
|
||||
"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",
|
||||
"@angular/platform-browser": "^21.0.0 || ^22.0.0",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cli": {
|
||||
"version": "21.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.4.tgz",
|
||||
@@ -560,6 +584,23 @@
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/material": {
|
||||
"version": "21.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/material/-/material-21.1.0.tgz",
|
||||
"integrity": "sha512-VFWUQMU5Rm8w6uW5+FcMbsDvHMmhviVxPsKAFdinJ4ySbm5c6z9c64nhlYCNRswRgLB1VcoVxEWitP77LUagYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/cdk": "21.1.0",
|
||||
"@angular/common": "^21.0.0 || ^22.0.0",
|
||||
"@angular/core": "^21.0.0 || ^22.0.0",
|
||||
"@angular/forms": "^21.0.0 || ^22.0.0",
|
||||
"@angular/platform-browser": "^21.0.0 || ^22.0.0",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser": {
|
||||
"version": "21.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.6.tgz",
|
||||
@@ -600,6 +641,25 @@
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/service-worker": {
|
||||
"version": "21.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-21.0.6.tgz",
|
||||
"integrity": "sha512-/T1aHc7ys3in7qTGO8MLIHvoXumMPxv7vU1C1sKbK14mw8ahwuqYo8m2Y+f6/ZcYwUZIbN3Ipd9sHEEB7VCz3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"ngsw-config": "ngsw-config.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "21.0.6",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz",
|
||||
@@ -948,6 +1008,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
@@ -1927,6 +1997,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/angular": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.7.15.tgz",
|
||||
"integrity": "sha512-2FFmOCoE3CRHR/WAsx+DX084ywxCNEdqrDuqy0iUmi1jbXrYFcC3xnbvBLKRsQtN56nFLS07DkL6wLCCOY7M4A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ionic/core": "8.7.15",
|
||||
"ionicons": "^8.0.13",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@angular/forms": ">=16.0.0",
|
||||
"@angular/router": ">=16.0.0",
|
||||
"rxjs": ">=7.5.0",
|
||||
"zone.js": ">=0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
"ionicons": "^8.0.13",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
@@ -2596,6 +2701,32 @@
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngx-translate/core": {
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-17.0.0.tgz",
|
||||
"integrity": "sha512-Rft2D5ns2pq4orLZjEtx1uhNuEBerUdpFUG1IcqtGuipj6SavgB8SkxtNQALNDA+EVlvsNCCjC2ewZVtUeN6rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16",
|
||||
"@angular/core": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngx-translate/http-loader": {
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-17.0.0.tgz",
|
||||
"integrity": "sha512-hgS8sa0ARjH9ll3PhkLTufeVXNI2DNR2uFKDhBgq13siUXzzVr/a31M6zgecrtwbA34iaBV01hsTMbMS8V7iIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16",
|
||||
"@angular/core": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@npmcli/agent": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz",
|
||||
@@ -3239,6 +3370,13 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-beta.47",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.47.tgz",
|
||||
@@ -3899,12 +4037,45 @@
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sqlite.org/sqlite-wasm": {
|
||||
"version": "3.44.0-build2",
|
||||
"resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.44.0-build2.tgz",
|
||||
"integrity": "sha512-zFfqHoYxuGQ15+A9W00JGQEfQ+f1b+NNpZRD98I31G0x7es+IiGIh8n52vBoub+q9bZLW++Xa6T1QXuw4eGSDQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"sqlite-wasm": "bin/index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
|
||||
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=7.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-darwin-arm64": "4.34.9",
|
||||
"@rollup/rollup-darwin-x64": "4.34.9",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.34.9",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.34.9",
|
||||
"@rollup/rollup-linux-x64-musl": "4.34.9",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.34.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@tufjs/canonical-json": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
||||
@@ -3978,17 +4149,105 @@
|
||||
"vite": "^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.17.tgz",
|
||||
"integrity": "sha512-cgf2JZk2fv5or3efmOrRJe1V9Md89BPgz4ntzbf84yAb+z2hW6niaGFinl9aFzPZ1q3TGfWZQWZ9gXTFThs2Qw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/mocker": "4.0.17",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"magic-string": "^0.30.21",
|
||||
"pixelmatch": "7.1.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"sirv": "^3.0.2",
|
||||
"tinyrainbow": "^3.0.3",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "4.0.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser-playwright": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.17.tgz",
|
||||
"integrity": "sha512-CE9nlzslHX6Qz//MVrjpulTC9IgtXTbJ+q7Rx1HD+IeSOWv4NHIRNHPA6dB4x01d9paEqt+TvoqZfmgq40DxEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/browser": "4.0.17",
|
||||
"@vitest/mocker": "4.0.17",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "*",
|
||||
"vitest": "4.0.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser/node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.17.tgz",
|
||||
"integrity": "sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"ast-v8-to-istanbul": "^0.3.10",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.2.0",
|
||||
"magicast": "^0.5.1",
|
||||
"obug": "^2.1.1",
|
||||
"std-env": "^3.10.0",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "4.0.17",
|
||||
"vitest": "4.0.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz",
|
||||
"integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz",
|
||||
"integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/utils": "4.0.16",
|
||||
"@vitest/spy": "4.0.17",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"chai": "^6.2.1",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
@@ -3997,13 +4256,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz",
|
||||
"integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz",
|
||||
"integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/spy": "4.0.17",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
@@ -4034,9 +4293,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz",
|
||||
"integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz",
|
||||
"integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4047,13 +4306,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz",
|
||||
"integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz",
|
||||
"integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.16",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -4061,13 +4320,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz",
|
||||
"integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz",
|
||||
"integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"@vitest/pretty-format": "4.0.17",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -4086,9 +4345,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz",
|
||||
"integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz",
|
||||
"integrity": "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -4096,13 +4355,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz",
|
||||
"integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.17.tgz",
|
||||
"integrity": "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"@vitest/pretty-format": "4.0.17",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -4211,6 +4470,22 @@
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/angular-web-sqlite": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/angular-web-sqlite/-/angular-web-sqlite-1.0.34.tgz",
|
||||
"integrity": "sha512-OfV/vA8FEBfb2SjbPB5529ZcjalAUf6WiI+RZcW3V/N1OgRNbM7HrxmIuYJCQ2cm8ZYO6LgOQRmN+ufeAIdM1g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sqlite.org/sqlite-wasm": "3.44.0-build2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "13.x || 14.x || 15.x || 16.x || 17.x || 18.x || 19.x || 20.x || 21.x || 22.x",
|
||||
"@angular/core": "13.x || 14.x || 15.x || 16.x || 17.x || 18.x || 19.x || 20.x || 21.x || 22.x",
|
||||
"@ionic/angular": "5.x || 6.x || 7.x || 8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
|
||||
@@ -4263,6 +4538,25 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "0.3.10",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz",
|
||||
"integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"estree-walker": "^3.0.3",
|
||||
"js-tokens": "^9.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.11",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||
@@ -5539,6 +5833,16 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -5601,6 +5905,13 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
|
||||
@@ -5754,6 +6065,16 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons": {
|
||||
"version": "8.0.13",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz",
|
||||
"integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.35.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
@@ -5916,6 +6237,35 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
|
||||
@@ -6020,7 +6370,6 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
|
||||
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonparse": {
|
||||
@@ -6208,6 +6557,34 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz",
|
||||
"integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/types": "^7.28.5",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-fetch-happen": {
|
||||
"version": "15.0.3",
|
||||
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz",
|
||||
@@ -7029,7 +7406,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"
|
||||
@@ -7083,7 +7459,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"
|
||||
@@ -7197,6 +7572,19 @@
|
||||
"@napi-rs/nice": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/pixelmatch": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz",
|
||||
"integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pngjs": "^7.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pixelmatch": "bin/pixelmatch"
|
||||
}
|
||||
},
|
||||
"node_modules/pkce-challenge": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
|
||||
@@ -7207,6 +7595,50 @@
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz",
|
||||
"integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz",
|
||||
"integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -7765,6 +8197,21 @@
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@polka/url": "^1.0.0-next.24",
|
||||
"mrmime": "^2.0.0",
|
||||
"totalist": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||
@@ -7983,6 +8430,19 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
@@ -8118,6 +8578,16 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
||||
@@ -8862,19 +9332,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz",
|
||||
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.17.tgz",
|
||||
"integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.16",
|
||||
"@vitest/mocker": "4.0.16",
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"@vitest/runner": "4.0.16",
|
||||
"@vitest/snapshot": "4.0.16",
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/utils": "4.0.16",
|
||||
"@vitest/expect": "4.0.17",
|
||||
"@vitest/mocker": "4.0.17",
|
||||
"@vitest/pretty-format": "4.0.17",
|
||||
"@vitest/runner": "4.0.17",
|
||||
"@vitest/snapshot": "4.0.17",
|
||||
"@vitest/spy": "4.0.17",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"expect-type": "^1.2.2",
|
||||
"magic-string": "^0.30.21",
|
||||
@@ -8902,10 +9372,10 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.0.16",
|
||||
"@vitest/browser-preview": "4.0.16",
|
||||
"@vitest/browser-webdriverio": "4.0.16",
|
||||
"@vitest/ui": "4.0.16",
|
||||
"@vitest/browser-playwright": "4.0.17",
|
||||
"@vitest/browser-preview": "4.0.17",
|
||||
"@vitest/browser-webdriverio": "4.0.17",
|
||||
"@vitest/ui": "4.0.17",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -9291,6 +9761,13 @@
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25 || ^4"
|
||||
}
|
||||
},
|
||||
"node_modules/zone.js": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.0.tgz",
|
||||
"integrity": "sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
package.json
@@ -3,8 +3,9 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"start": "ng serve --host 0.0.0.0 --ssl true --ssl-key \"./cert/server.key\" --ssl-cert \"./cert/server.crt\"",
|
||||
"build": "env NG_BUILD_MANGLE=false ng build",
|
||||
"build:dev": "env NG_BUILD_MANGLE=false ng build && scp -P 8022 -r /home/delosrios/programming/groceries-price-tracker/groceries-price-tracker/dist/groceries-price-tracker/browser/** gabriel@192.168.1.4:/data/data/com.termux/files/usr/share/nginx/html/groceries-price-tracker",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
@@ -23,12 +24,18 @@
|
||||
"private": true,
|
||||
"packageManager": "npm@11.5.1",
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^21.1.0",
|
||||
"@angular/common": "^21.0.0",
|
||||
"@angular/compiler": "^21.0.0",
|
||||
"@angular/core": "^21.0.0",
|
||||
"@angular/forms": "^21.0.0",
|
||||
"@angular/material": "^21.1.0",
|
||||
"@angular/platform-browser": "^21.0.0",
|
||||
"@angular/router": "^21.0.0",
|
||||
"@angular/service-worker": "^21.0.6",
|
||||
"@ngx-translate/core": "^17.0.0",
|
||||
"@ngx-translate/http-loader": "^17.0.0",
|
||||
"angular-web-sqlite": "^1.0.34",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -36,6 +43,8 @@
|
||||
"@angular/build": "^21.0.4",
|
||||
"@angular/cli": "^21.0.4",
|
||||
"@angular/compiler-cli": "^21.0.0",
|
||||
"@vitest/browser-playwright": "^4.0.17",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"jsdom": "^27.1.0",
|
||||
"typescript": "~5.9.2",
|
||||
"vitest": "^4.0.8"
|
||||
|
||||
126
public/barcode-reader/barcode-reader.css
Normal file
@@ -0,0 +1,126 @@
|
||||
:host {
|
||||
display: flex;
|
||||
--video-width: 100%;
|
||||
--video-height: 100dvh;
|
||||
--video-background-color: #cccccc;
|
||||
--dialog-background-color: #ffffff;
|
||||
--dialog-backdrop-background-color: #cecece;
|
||||
}
|
||||
|
||||
#container {
|
||||
display: none;
|
||||
position: relative;
|
||||
width: var(--video-width);
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border-radius: 50%;
|
||||
border-style: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-right: 12px;
|
||||
margin-top: 12px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
#container {
|
||||
min-width: 400px;
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
#video-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: var(--video-height);
|
||||
max-height: var(--video-height);
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
#data-canvas {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#frame {
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: transparent;
|
||||
transform-origin: center center;
|
||||
transform: scale(0.9);
|
||||
transition: transform .3s ease-out;
|
||||
}
|
||||
|
||||
:host([capturing]) #frame {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
:host([no-frame]) #frame {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.corner {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 3px solid #ee0b0b;
|
||||
clip-path: polygon(0 48px, 48px 48px, 48px 0, 0 0);
|
||||
}
|
||||
|
||||
.top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 31%;
|
||||
left: 2%;
|
||||
width: 96%;
|
||||
height: 23%;
|
||||
border-radius: 2%;
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 0 200vmax rgba(0, 0, 0, 0.6);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
49
public/i18n/en.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"navbar": {
|
||||
"label": {
|
||||
"home": "home",
|
||||
"register": "register",
|
||||
"settings": "settings",
|
||||
"budget": "budget"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "settings",
|
||||
"nav": {
|
||||
"language": "language",
|
||||
"manage_chains": "manage chains",
|
||||
"manage_establishments": "manage establishments",
|
||||
"manage_products": "manage products"
|
||||
},
|
||||
"language": {
|
||||
"title": "language",
|
||||
"english": "english",
|
||||
"spanish": "spanish",
|
||||
"portuguese": "portuguese"
|
||||
},
|
||||
"chain": {
|
||||
"chain": "chain",
|
||||
"chains": "chains",
|
||||
"new_chain": "new chain",
|
||||
"edit_chain": "edit chain"
|
||||
},
|
||||
"establishment": {
|
||||
"establishments":"establishments",
|
||||
"new_establishment": "new establishment",
|
||||
"edit_establishment": "edit establishment"
|
||||
},
|
||||
"product": {
|
||||
"products":"products",
|
||||
"new_product":"new product",
|
||||
"edit_product":"edit product"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"address":"address",
|
||||
"barcode":"barcode",
|
||||
"name":"name",
|
||||
"save": "save",
|
||||
"update": "update",
|
||||
"no_file_yet": "no file upload yet"
|
||||
}
|
||||
}
|
||||
49
public/i18n/es.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"navbar": {
|
||||
"label": {
|
||||
"home": "inicio",
|
||||
"register": "ingresar",
|
||||
"settings": "ajustes",
|
||||
"budget": "presupuesto"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "ajustes",
|
||||
"nav": {
|
||||
"language": "idioma",
|
||||
"manage_chains": "administrar cadenas",
|
||||
"manage_establishments": "administrar establecimientos",
|
||||
"manage_products": "administrar productos"
|
||||
},
|
||||
"language": {
|
||||
"title": "idioma",
|
||||
"english": "inglés",
|
||||
"spanish": "español",
|
||||
"portuguese": "portugués"
|
||||
},
|
||||
"chain": {
|
||||
"chain": "cadena",
|
||||
"chains": "cadenas",
|
||||
"new_chain": "nueva cadena",
|
||||
"edit_chain": "editar cadena"
|
||||
},
|
||||
"establishment": {
|
||||
"establishments":"establecimientos",
|
||||
"new_establishment": "nuevo establecimiento",
|
||||
"edit_establishment": "editar establecimiento"
|
||||
},
|
||||
"product": {
|
||||
"products":"productos",
|
||||
"new_product":"nuevo producto",
|
||||
"edit_product":"editar producto"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"address":"dirección",
|
||||
"barcode":"código de barras",
|
||||
"name":"nombre",
|
||||
"no_file_yet": "Sin carga",
|
||||
"save": "guardar",
|
||||
"update": "actualizar"
|
||||
}
|
||||
}
|
||||
49
public/i18n/pt.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"navbar": {
|
||||
"label": {
|
||||
"home": "home",
|
||||
"register": "register",
|
||||
"settings": "settings",
|
||||
"budget": "budget"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "settings",
|
||||
"nav": {
|
||||
"language": "language",
|
||||
"manage_chains": "manage chains",
|
||||
"manage_establishments": "manage establishments",
|
||||
"manage_products": "manage products"
|
||||
},
|
||||
"language": {
|
||||
"title": "language",
|
||||
"english": "english",
|
||||
"spanish": "spanish",
|
||||
"portuguese": "portuguese"
|
||||
},
|
||||
"chain": {
|
||||
"chain": "chain",
|
||||
"chains": "chains",
|
||||
"new_chain": "new chain",
|
||||
"edit_chain": "edit chain"
|
||||
},
|
||||
"establishment": {
|
||||
"establishments":"establishments",
|
||||
"new_establishment": "new establishment",
|
||||
"edit_establishment": "edit establishment"
|
||||
},
|
||||
"product": {
|
||||
"products":"products",
|
||||
"new_product":"new product",
|
||||
"edit_product":"edit product"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"address":"address",
|
||||
"barcode":"barcode",
|
||||
"name":"name",
|
||||
"save": "save",
|
||||
"update": "update",
|
||||
"no_file_yet": "no file upload yet"
|
||||
}
|
||||
}
|
||||
BIN
public/icons/flags/br.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/icons/flags/sp.png
Normal file
|
After Width: | Height: | Size: 582 B |
BIN
public/icons/flags/us.png
Normal file
|
After Width: | Height: | Size: 994 B |
BIN
public/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/icons/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/icons/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/icons/icon-48x48.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
public/icons/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/icons/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
63
public/manifest.webmanifest
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "Groceries price tracker",
|
||||
"short_name": "Price tracker",
|
||||
"display": "standalone",
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
}
|
||||
]
|
||||
}
|
||||
1034
public/scan-loading-screen/scan-loading-screen.js
Normal file
@@ -1,11 +1,45 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import {
|
||||
ApplicationConfig,
|
||||
inject,
|
||||
provideAppInitializer,
|
||||
provideBrowserGlobalErrorListeners,
|
||||
isDevMode,
|
||||
} from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { WebSqlite } from 'angular-web-sqlite';
|
||||
import { Sqlite } from './services/sqlite';
|
||||
import { tables } from '../migrations/20260117';
|
||||
import { provideServiceWorker } from '@angular/service-worker';
|
||||
import { provideTranslateService, TranslateService } from '@ngx-translate/core';
|
||||
import { provideTranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideAppInitializer(async () => {
|
||||
const sqlite = inject(Sqlite);
|
||||
const translateService = inject(TranslateService);
|
||||
translateService.addLangs(['en', 'es', 'pt']);
|
||||
translateService.setFallbackLang('es');
|
||||
await sqlite.initializeDatabase('gptdb');
|
||||
await sqlite.batchSqlOperations(tables);
|
||||
await sqlite.executeQuery('PRAGMA foreign_keys = ON;');
|
||||
document.dispatchEvent(new CustomEvent('ng-boot'));
|
||||
}),
|
||||
{ provide: WebSqlite, useClass: WebSqlite },
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideRouter(routes)
|
||||
]
|
||||
provideRouter(routes),
|
||||
provideServiceWorker('ngsw-worker.js', {
|
||||
enabled: !isDevMode(),
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
provideTranslateService({
|
||||
loader: provideTranslateHttpLoader({
|
||||
prefix: '/i18n/',
|
||||
suffix: '.json',
|
||||
}),
|
||||
fallbackLang: 'en',
|
||||
lang: 'en',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
342
src/app/app.html
@@ -1,342 +1,4 @@
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
|
||||
<!-- * * * * * * * to get started with your project! * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
|
||||
<style>
|
||||
:host {
|
||||
--bright-blue: oklch(51.01% 0.274 263.83);
|
||||
--electric-violet: oklch(53.18% 0.28 296.97);
|
||||
--french-violet: oklch(47.66% 0.246 305.88);
|
||||
--vivid-pink: oklch(69.02% 0.277 332.77);
|
||||
--hot-red: oklch(61.42% 0.238 15.34);
|
||||
--orange-red: oklch(63.32% 0.24 31.68);
|
||||
|
||||
--gray-900: oklch(19.37% 0.006 300.98);
|
||||
--gray-700: oklch(36.98% 0.014 302.71);
|
||||
--gray-400: oklch(70.9% 0.015 304.04);
|
||||
|
||||
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
|
||||
180deg,
|
||||
var(--orange-red) 0%,
|
||||
var(--vivid-pink) 50%,
|
||||
var(--electric-violet) 100%
|
||||
);
|
||||
|
||||
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
|
||||
90deg,
|
||||
var(--orange-red) 0%,
|
||||
var(--vivid-pink) 50%,
|
||||
var(--electric-violet) 100%
|
||||
);
|
||||
|
||||
--pill-accent: var(--bright-blue);
|
||||
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol";
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.125rem;
|
||||
color: var(--gray-900);
|
||||
font-weight: 500;
|
||||
line-height: 100%;
|
||||
letter-spacing: -0.125rem;
|
||||
margin: 0;
|
||||
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol";
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
box-sizing: inherit;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.angular-logo {
|
||||
max-width: 9.2rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
background: var(--red-to-pink-to-purple-vertical-gradient);
|
||||
margin-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.pill-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--pill-accent: var(--bright-blue);
|
||||
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
|
||||
color: var(--pill-accent);
|
||||
padding-inline: 0.75rem;
|
||||
padding-block: 0.375rem;
|
||||
border-radius: 2.75rem;
|
||||
border: 0;
|
||||
transition: background 0.3s ease;
|
||||
font-family: var(--inter-font);
|
||||
font-size: 0.875rem;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 1.4rem;
|
||||
letter-spacing: -0.00875rem;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pill:hover {
|
||||
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
|
||||
}
|
||||
|
||||
.pill-group .pill:nth-child(6n + 1) {
|
||||
--pill-accent: var(--bright-blue);
|
||||
}
|
||||
.pill-group .pill:nth-child(6n + 2) {
|
||||
--pill-accent: var(--electric-violet);
|
||||
}
|
||||
.pill-group .pill:nth-child(6n + 3) {
|
||||
--pill-accent: var(--french-violet);
|
||||
}
|
||||
|
||||
.pill-group .pill:nth-child(6n + 4),
|
||||
.pill-group .pill:nth-child(6n + 5),
|
||||
.pill-group .pill:nth-child(6n + 6) {
|
||||
--pill-accent: var(--hot-red);
|
||||
}
|
||||
|
||||
.pill-group svg {
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.73rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.social-links path {
|
||||
transition: fill 0.3s ease;
|
||||
fill: var(--gray-400);
|
||||
}
|
||||
|
||||
.social-links a:hover svg path {
|
||||
fill: var(--gray-900);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.content {
|
||||
flex-direction: column;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: var(--red-to-pink-to-purple-horizontal-gradient);
|
||||
margin-block: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<main class="main">
|
||||
<div class="content">
|
||||
<div class="left-side">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 982 239"
|
||||
fill="none"
|
||||
class="angular-logo"
|
||||
>
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#c)"
|
||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="c"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FF41F8" />
|
||||
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5" />
|
||||
<stop offset="1" stop-color="#FF41F8" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1="0"
|
||||
x2="982"
|
||||
y1="192"
|
||||
y2="192"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F0060B" />
|
||||
<stop offset="0" stop-color="#F0070C" />
|
||||
<stop offset=".526" stop-color="#CC26D5" />
|
||||
<stop offset="1" stop-color="#7702FF" />
|
||||
</linearGradient>
|
||||
<clipPath id="a"><path fill="#fff" d="M0 0h982v239H0z" /></clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<h1>Hello, {{ title() }}</h1>
|
||||
<p>Congratulations! Your app is running. 🎉</p>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<div class="divider" role="separator" aria-label="Divider"></div>
|
||||
<div class="right-side">
|
||||
<div class="pill-group">
|
||||
@for (item of [
|
||||
{ title: 'Explore the Docs', link: 'https://angular.dev' },
|
||||
{ title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
|
||||
{ title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
|
||||
{ title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
|
||||
{ title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
|
||||
{ title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
|
||||
]; track item.title) {
|
||||
<a
|
||||
class="pill"
|
||||
[href]="item.link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<span>{{ item.title }}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="14"
|
||||
viewBox="0 -960 960 960"
|
||||
width="14"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="social-links">
|
||||
<a
|
||||
href="https://github.com/angular/angular"
|
||||
aria-label="Github"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="Github"
|
||||
>
|
||||
<path
|
||||
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://x.com/angular"
|
||||
aria-label="X"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="X"
|
||||
>
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
|
||||
aria-label="Youtube"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
width="29"
|
||||
height="20"
|
||||
viewBox="0 0 29 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="Youtube"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
|
||||
|
||||
<router-outlet />
|
||||
<app-bottom-navigation-bar [options]="menuOptions"/>
|
||||
@@ -1,3 +1,30 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export const routes: Routes = [];
|
||||
import { Home } from './pages/home/home';
|
||||
import { routes as settingsRoutes } from './pages/settings/settings.route'
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'home',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: 'register',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: 'budget',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
children: settingsRoutes
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'home',
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'home',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: calc(100% - 56px);
|
||||
max-height: calc(100vh - 56px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
app-bottom-navigation-bar {
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { App } from './app';
|
||||
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
providers: [provideTranslateService({})],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -13,11 +16,4 @@ describe('App', () => {
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render title', async () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, groceries-price-tracker');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { BottomNavigationBar } from './components/bottom-navigation-bar/bottom-navigation-bar';
|
||||
import { BottomNavigationBarOption } from './components/bottom-navigation-bar/BottomNavigationBarOption';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
imports: [RouterOutlet, BottomNavigationBar],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.scss'
|
||||
styleUrls: [
|
||||
'./app.scss'
|
||||
],
|
||||
})
|
||||
export class App {
|
||||
protected readonly title = signal('groceries-price-tracker');
|
||||
|
||||
protected menuOptions = [
|
||||
new BottomNavigationBarOption('navbar.label.home', 'home', '/home'),
|
||||
new BottomNavigationBarOption(
|
||||
'navbar.label.register',
|
||||
'barcode',
|
||||
'/register',
|
||||
'material-symbols-outlined',
|
||||
),
|
||||
new BottomNavigationBarOption(
|
||||
'navbar.label.budget',
|
||||
'barcode_reader',
|
||||
'/budget',
|
||||
'material-symbols-outlined',
|
||||
),
|
||||
new BottomNavigationBarOption('navbar.label.settings', 'settings', '/settings'),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
3
src/app/components/action-btn/action-btn.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="action-btn">
|
||||
<button matButton="outlined" [disabled]="disabled()">{{text()}}</button>
|
||||
</div>
|
||||
12
src/app/components/action-btn/action-btn.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
:host {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 24px 0;
|
||||
width: 100%;
|
||||
button {
|
||||
width: 100%;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
23
src/app/components/action-btn/action-btn.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActionBtn } from './action-btn';
|
||||
|
||||
describe('ActionBtn', () => {
|
||||
let component: ActionBtn;
|
||||
let fixture: ComponentFixture<ActionBtn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActionBtn]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActionBtn);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
13
src/app/components/action-btn/action-btn.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
|
||||
@Component({
|
||||
selector: 'app-action-btn',
|
||||
imports: [MatButton],
|
||||
templateUrl: './action-btn.html',
|
||||
styleUrl: './action-btn.scss',
|
||||
})
|
||||
export class ActionBtn {
|
||||
disabled = input<boolean>(true);
|
||||
text = input('');
|
||||
}
|
||||
15
src/app/components/bar-code-input/bar-code-input.html
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
<div class="bar-code-input__container">
|
||||
<div class="bar-code-input__input">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{'common.barcode'|translate|upperfirst}}</mat-label>
|
||||
<input matInput #input [formControl]="control"/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="bar-code-input__btn">
|
||||
<button matMiniFab color="primary" class="upload-btn">
|
||||
<mat-icon [fontSet]="'material-symbols-outlined'" (click)="scan.set({scan: true})">barcode_scanner</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-bar-code-reader [scan]="scan()" (result)="updateBarcode($event)"></app-bar-code-reader>
|
||||
18
src/app/components/bar-code-input/bar-code-input.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
.bar-code-input {
|
||||
&__container {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
65
src/app/components/bar-code-input/bar-code-input.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BarCodeInput } from './bar-code-input';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { DetectedBarcode } from '../../types/globalThis';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-code-input-mock',
|
||||
imports: [BarCodeInput, ReactiveFormsModule],
|
||||
template: `
|
||||
<form [formGroup]="form">
|
||||
<app-bar-code-input formControlName="mock" />
|
||||
</form>
|
||||
`,
|
||||
styleUrl: './bar-code-input.scss',
|
||||
})
|
||||
class BarCodeInputTestbed {
|
||||
form = new FormGroup({ mock: new FormControl(null) });
|
||||
}
|
||||
|
||||
describe('BarCodeInput', () => {
|
||||
let component: BarCodeInputTestbed;
|
||||
let fixture: ComponentFixture<BarCodeInputTestbed>;
|
||||
|
||||
let BARCODE_MOCK: {
|
||||
code: Partial<DetectedBarcode> | null;
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BarCodeInputTestbed],
|
||||
providers: [provideTranslateService()],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BarCodeInputTestbed);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('on result', () => {
|
||||
it('should patch input value with barcode value ', () => {
|
||||
const patchValueSpy = vi.spyOn(component.form.controls.mock, 'patchValue');
|
||||
const markAsDirtySpy = vi.spyOn(component.form.controls.mock, 'markAsDirty');
|
||||
BARCODE_MOCK = {code: {'rawValue': 'mock'}}
|
||||
const barcodeReader = fixture.debugElement.query(By.css('app-bar-code-reader'));
|
||||
barcodeReader.triggerEventHandler('result', new CustomEvent('result', {detail: BARCODE_MOCK}));
|
||||
expect(patchValueSpy).toHaveBeenCalledExactlyOnceWith(BARCODE_MOCK.code?.rawValue);
|
||||
expect(markAsDirtySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not patch input value if barcode value is null', () => {
|
||||
const patchValueSpy = vi.spyOn(component.form.controls.mock, 'patchValue');
|
||||
BARCODE_MOCK = {code: null}
|
||||
const barcodeReader = fixture.debugElement.query(By.css('app-bar-code-reader'));
|
||||
barcodeReader.triggerEventHandler('result', new CustomEvent('result', {detail: BARCODE_MOCK}));
|
||||
expect(patchValueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
60
src/app/components/bar-code-input/bar-code-input.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Component, OnInit, Optional, Self, signal } from '@angular/core';
|
||||
import { MatMiniFabButton } from '@angular/material/button';
|
||||
import { MatFormField, MatLabel } from '@angular/material/form-field';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { BarCodeReaderWrapper } from '../bar-code-reader/bar-code-reader';
|
||||
import { ControlValueAccessor, FormControl, NgControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { DetectedBarcode } from '../../types/globalThis';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-code-input',
|
||||
imports: [
|
||||
BarCodeReaderWrapper,
|
||||
MatFormField,
|
||||
MatIcon,
|
||||
MatInput,
|
||||
MatLabel,
|
||||
MatMiniFabButton,
|
||||
ReactiveFormsModule,
|
||||
TranslatePipe,
|
||||
UpperfirstPipe,
|
||||
],
|
||||
templateUrl: './bar-code-input.html',
|
||||
styleUrl: './bar-code-input.scss',
|
||||
})
|
||||
export class BarCodeInput implements ControlValueAccessor, OnInit {
|
||||
scan = signal({ scan: false });
|
||||
protected control = new FormControl();
|
||||
|
||||
constructor(@Self() @Optional() public controlDir: NgControl) {
|
||||
if (this.controlDir) {
|
||||
this.controlDir.valueAccessor = this;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.control = <FormControl>this.controlDir?.control;
|
||||
}
|
||||
|
||||
updateBarcode(
|
||||
barcodeEvent: Event & {
|
||||
detail?: {
|
||||
code: DetectedBarcode | null;
|
||||
};
|
||||
},
|
||||
) {
|
||||
let code = barcodeEvent.detail?.code?.rawValue
|
||||
if(code) {
|
||||
this.control.patchValue(code);
|
||||
this.control.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
writeValue(obj: any): void {}
|
||||
registerOnChange(fn: any): void {}
|
||||
registerOnTouched(fn: any): void {}
|
||||
setDisabledState?(isDisabled: boolean): void {}
|
||||
}
|
||||
7
src/app/components/bar-code-reader/bar-code-reader.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<barcode-reader
|
||||
[style.display]="scan().scan ? 'block' : 'none'"
|
||||
#reader
|
||||
(result)="this.result.emit($event)"
|
||||
(scan-status)="this.scanStatus.emit($event)"
|
||||
(scan-permission-denied)="this.scanPermissionDenied.emit()"
|
||||
/>
|
||||
8
src/app/components/bar-code-reader/bar-code-reader.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
barcode-reader {
|
||||
display: block;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
23
src/app/components/bar-code-reader/bar-code-reader.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BarCodeReaderWrapper } from './bar-code-reader';
|
||||
|
||||
describe('BarCodeReaderWrapper', () => {
|
||||
let component: BarCodeReaderWrapper;
|
||||
let fixture: ComponentFixture<BarCodeReaderWrapper>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BarCodeReaderWrapper]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BarCodeReaderWrapper);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
23
src/app/components/bar-code-reader/bar-code-reader.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA, effect, ElementRef, input, output, signal, viewChild } from '@angular/core';
|
||||
import { DetectedBarcode } from '../../types/globalThis';
|
||||
import { BarcodeReader } from '../../web-components/barcode-reader';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-code-reader',
|
||||
templateUrl: './bar-code-reader.html',
|
||||
styleUrl: './bar-code-reader.scss',
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
export class BarCodeReaderWrapper {
|
||||
private readonly ref = viewChild<ElementRef<BarcodeReader>>('reader');
|
||||
protected readonly result = output<Event & {detail?: {code: DetectedBarcode | null}}>();
|
||||
protected readonly scanStatus = output<Event & {detail?: {state: ScanState}}>();
|
||||
protected readonly scanPermissionDenied = output();
|
||||
scan = input({scan: false})
|
||||
|
||||
constructor() {
|
||||
effect(() => this.scan().scan && this.ref()?.nativeElement.scan())
|
||||
}
|
||||
}
|
||||
|
||||
export type ScanState = 'loading'|'started'|'stopped';
|
||||
@@ -0,0 +1,8 @@
|
||||
export class BottomNavigationBarOption {
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly icon: string,
|
||||
public readonly path: string,
|
||||
public readonly fontSet: string = '',
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<mat-toolbar class="bottom-nav">
|
||||
@for(option of options(); track option.path) {
|
||||
<button [routerLink]="option.path" mat-button class="bottom-nav__item" routerLinkActive="bottom-nav__item--selected">
|
||||
<mat-icon [fontSet]="option.fontSet">{{option.icon}}</mat-icon>
|
||||
<span>{{option.label|translate|titlecase}}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-toolbar>
|
||||
@@ -0,0 +1,37 @@
|
||||
.bottom-nav {
|
||||
align-items: center;
|
||||
background: var(--mat-sys-primary-container);
|
||||
display: flex;
|
||||
height: 56px;
|
||||
justify-content: space-around;
|
||||
|
||||
&__item {
|
||||
align-items: center;
|
||||
background-color: unset;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& mat-icon {
|
||||
color: rgba(#ffffff, 0.6);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
& span {
|
||||
color: rgba(#ffffff, 0.6);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
& mat-icon {
|
||||
color: #ffffff;
|
||||
}
|
||||
& span {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BottomNavigationBar } from './bottom-navigation-bar';
|
||||
|
||||
describe('BottomNavigationBar', () => {
|
||||
let component: BottomNavigationBar;
|
||||
let fixture: ComponentFixture<BottomNavigationBar>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BottomNavigationBar]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BottomNavigationBar);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { TitleCasePipe } from '@angular/common';
|
||||
import { Component, input } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { BottomNavigationBarOption } from './BottomNavigationBarOption';
|
||||
import { RouterLink, RouterLinkActive } from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-bottom-navigation-bar',
|
||||
imports: [MatToolbarModule, MatIconModule, TranslatePipe, TitleCasePipe, RouterLink, RouterLinkActive],
|
||||
templateUrl: './bottom-navigation-bar.html',
|
||||
styleUrl: './bottom-navigation-bar.scss',
|
||||
})
|
||||
export class BottomNavigationBar {
|
||||
readonly options = input<BottomNavigationBarOption[]>([]);
|
||||
}
|
||||
51
src/app/components/chain-add/chain-add.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChainAdd } from './chain-add';
|
||||
import { vi } from 'vitest';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { ChainFormGroup } from '../../pages/settings/chains/chain-formgroup';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ChainSettings } from '../../services/chain-settings';
|
||||
import { Chain } from '../../models/Chain';
|
||||
|
||||
describe('ChainAdd', () => {
|
||||
let component: ChainAdd;
|
||||
let fixture: ComponentFixture<ChainAdd>;
|
||||
|
||||
let chainSettings: Partial<ChainSettings>;
|
||||
|
||||
beforeEach(async () => {
|
||||
chainSettings = {
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ChainAdd],
|
||||
providers: [provideTranslateService(), { provide: ChainSettings, useValue: chainSettings }],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChainAdd);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create', async () => {
|
||||
await fixture.whenStable();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should insert chain and store image', async () => {
|
||||
(<any>component).form = new ChainFormGroup({
|
||||
name: new FormControl('Mock'),
|
||||
image: new FormControl(new File([], 'mock')),
|
||||
});
|
||||
await fixture.whenStable();
|
||||
const actionBtn = fixture.debugElement.query(By.css('app-action-btn'));
|
||||
actionBtn.triggerEventHandler('click');
|
||||
await fixture.whenStable();
|
||||
expect(chainSettings.save).toHaveBeenCalledExactlyOnceWith(
|
||||
new Chain(component.form.controls.name.value, ''),
|
||||
component.form.controls.image.value,
|
||||
);
|
||||
});
|
||||
});
|
||||
34
src/app/components/chain-add/chain-add.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Chain } from '../../models/Chain';
|
||||
import { ChainFormGroup } from '../../pages/settings/chains/chain-formgroup';
|
||||
import { ActionBtn } from '../action-btn/action-btn';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||
import { ChainSettings } from '../../services/chain-settings';
|
||||
import { SettingsBaseAddEdit } from '../settings-base-add-edit/settings-base-add-edit';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chain-add',
|
||||
imports: [
|
||||
ActionBtn,
|
||||
TranslatePipe,
|
||||
UpperfirstPipe,
|
||||
],
|
||||
templateUrl: './../settings-base-add-edit/settings-base-add-edit.html',
|
||||
styleUrl: './../settings-base-add-edit/settings-base-add-edit.scss',
|
||||
})
|
||||
export class ChainAdd extends SettingsBaseAddEdit {
|
||||
private readonly chainSettings = inject(ChainSettings);
|
||||
|
||||
readonly form = new ChainFormGroup();
|
||||
btnText = 'common.save';
|
||||
title = 'settings.chain.new_chain';
|
||||
|
||||
async submit() {
|
||||
const name = this.form.controls.name.value;
|
||||
const img = this.form.controls.image.value;
|
||||
//TODO: the sqlite bridge can't handle null as param
|
||||
const chain = new Chain(name, '');
|
||||
await this.chainSettings.save(chain, img);
|
||||
}
|
||||
}
|
||||
79
src/app/components/chain-edit/chain-edit.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChainEdit } from './chain-edit';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Chain } from '../../models/Chain';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ImageStorage } from '../../services/image-storage';
|
||||
import { ChainSettings } from '../../services/chain-settings';
|
||||
|
||||
const CHAIN_MOCK = new Chain('Mock', '', 1);
|
||||
describe('ChainEdit', () => {
|
||||
let component: ChainEdit;
|
||||
let fixture: ComponentFixture<ChainEdit>;
|
||||
|
||||
let activatedRoute: Partial<ActivatedRoute>;
|
||||
let chainSettings: Partial<ChainSettings>;
|
||||
let imageStorage: Partial<ImageStorage>;
|
||||
|
||||
const dataSubject = new BehaviorSubject({ chain: CHAIN_MOCK });
|
||||
beforeEach(async () => {
|
||||
activatedRoute = {
|
||||
data: dataSubject,
|
||||
};
|
||||
chainSettings = {
|
||||
update: vi.fn(),
|
||||
};
|
||||
imageStorage = {
|
||||
getImage: vi.fn(),
|
||||
saveImage: vi.fn(),
|
||||
deleteImage: vi.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ChainEdit],
|
||||
providers: [
|
||||
provideTranslateService(),
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: ChainSettings, useValue: chainSettings },
|
||||
{ provide: ImageStorage, useValue: imageStorage },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChainEdit);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create', async () => {
|
||||
await fixture.whenStable();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call chainSettings update on chain update', async () => {
|
||||
const CHAIN_NAME_UPDATE_MOCK = 'name update mock';
|
||||
await fixture.whenStable();
|
||||
//User updates name input field
|
||||
component.form.controls.name.patchValue(CHAIN_NAME_UPDATE_MOCK)
|
||||
fixture.whenStable();
|
||||
const actionBtn = fixture.debugElement.query(By.css('app-action-btn'));
|
||||
actionBtn.triggerEventHandler('click');
|
||||
await fixture.whenStable();
|
||||
expect(chainSettings.update).toHaveBeenCalledExactlyOnceWith(
|
||||
CHAIN_MOCK,
|
||||
new Chain(CHAIN_NAME_UPDATE_MOCK, component['chain']!.image, component['chain']!.id),
|
||||
component.form.controls.image.value,
|
||||
);
|
||||
});
|
||||
|
||||
it('should patch form with chain data', async () => {
|
||||
const IMAGE_FILE_MOCK = new Blob([], { type: 'image/png' });
|
||||
imageStorage.getImage = vi.fn().mockResolvedValue(IMAGE_FILE_MOCK);
|
||||
const CHAIN_MOCK = new Chain('name', 'image.png', 1);
|
||||
dataSubject.next({ chain: CHAIN_MOCK });
|
||||
await fixture.whenStable();
|
||||
expect(component.form.controls.name.value).toEqual(CHAIN_MOCK.name);
|
||||
expect(component.form.controls.image.value?.type).toEqual(IMAGE_FILE_MOCK.type);
|
||||
});
|
||||
});
|
||||
63
src/app/components/chain-edit/chain-edit.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { ChainFormGroup } from '../../pages/settings/chains/chain-formgroup';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable, take, tap } from 'rxjs';
|
||||
import { Chain } from '../../models/Chain';
|
||||
import { ImageStorage } from '../../services/image-storage';
|
||||
import { ActionBtn } from '../action-btn/action-btn';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||
import { ChainSettings } from '../../services/chain-settings';
|
||||
import { SettingsBaseAddEdit } from '../settings-base-add-edit/settings-base-add-edit';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chain-edit',
|
||||
imports: [ActionBtn, TranslatePipe, UpperfirstPipe],
|
||||
templateUrl: './../settings-base-add-edit/settings-base-add-edit.html',
|
||||
styleUrl: './../settings-base-add-edit/settings-base-add-edit.scss',
|
||||
})
|
||||
export class ChainEdit extends SettingsBaseAddEdit implements OnInit {
|
||||
|
||||
protected readonly activatedRoute = inject(ActivatedRoute);
|
||||
private readonly imageStorage = inject(ImageStorage);
|
||||
private readonly chainSettings = inject(ChainSettings);
|
||||
|
||||
private chain?: Chain;
|
||||
btnText = 'common.update';
|
||||
title = 'settings.chain.edit_chain';
|
||||
readonly form = new ChainFormGroup();
|
||||
|
||||
ngOnInit() {
|
||||
(<Observable<{ chain: Chain }>>this.activatedRoute.data)
|
||||
.pipe(
|
||||
take(1),
|
||||
tap((data) => (this.chain = new Chain(data.chain.name, data.chain.image, data.chain.id))),
|
||||
tap((data) => this.patchForm(data.chain)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async patchForm(chain: Chain) {
|
||||
try {
|
||||
this.form.controls.name.patchValue(chain.name);
|
||||
if (chain.image) {
|
||||
const imgName = chain.image;
|
||||
const blob = await this.imageStorage.getImage(imgName);
|
||||
const file = new File([blob], imgName, { type: blob.type });
|
||||
this.form.controls.image.patchValue(file);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//TODO: reportar error
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.chain) {
|
||||
const updatedName = this.form.controls.name.value;
|
||||
const updatedImg = this.form.controls.image.value;
|
||||
const updatedChain = new Chain(updatedName, '', this.chain.id);
|
||||
await this.chainSettings.update(this.chain, updatedChain, updatedImg);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/app/components/chain-select/chain-select.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<mat-form-field>
|
||||
<mat-label>{{'settings.chain.chain'|translate|upperfirst}}</mat-label>
|
||||
<mat-select
|
||||
[formControl]="control"
|
||||
[compareWith]="compareFn"
|
||||
>
|
||||
@for(chain of chains$|async; track chain.id) {
|
||||
<mat-option [value]="chain">{{chain.name}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
3
src/app/components/chain-select/chain-select.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
65
src/app/components/chain-select/chain-select.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { MatSelect } from '@angular/material/select';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { Chain } from '../../models/Chain';
|
||||
import { ChainDAO } from '../../dao/ChainDAO';
|
||||
import { ChainSelect } from './chain-select';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chain-select-mock',
|
||||
imports: [
|
||||
ChainSelect,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
template: `
|
||||
<form [formGroup]="form">
|
||||
<app-chain-select formControlName="mock"/>
|
||||
</form>
|
||||
|
||||
`,
|
||||
styleUrl: './chain-select.scss',
|
||||
})
|
||||
class ChainSelectTestbed {
|
||||
form = new FormGroup({mock: new FormControl()})
|
||||
}
|
||||
|
||||
describe('ChainSelect', () => {
|
||||
let component: ChainSelectTestbed;
|
||||
let fixture: ComponentFixture<ChainSelectTestbed>;
|
||||
|
||||
let chainDAO: Partial<ChainDAO>;
|
||||
let CHAINS_MOCK: Chain[];
|
||||
beforeEach(async () => {
|
||||
CHAINS_MOCK = [new Chain('Mock 1', '', 1)];
|
||||
|
||||
chainDAO = {
|
||||
findAll: vi.fn().mockResolvedValue(CHAINS_MOCK),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ChainSelectTestbed],
|
||||
providers: [{ provide: ChainDAO, useValue: chainDAO }, provideTranslateService()],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChainSelectTestbed);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
});
|
||||
|
||||
it('should create', async () => {
|
||||
await fixture.whenStable();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should match form chain with chain option', async () => {
|
||||
component.form = new FormGroup({mock: new FormControl(CHAINS_MOCK[0])});
|
||||
await fixture.whenStable();
|
||||
const matSelect: MatSelect = fixture.debugElement.query(By.css('mat-select')).componentInstance;
|
||||
expect(matSelect.value).toEqual(CHAINS_MOCK[0]);
|
||||
});
|
||||
|
||||
});
|
||||
53
src/app/components/chain-select/chain-select.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Component, inject, OnInit, Optional, Self } from '@angular/core';
|
||||
import { ChainDAO } from '../../dao/ChainDAO';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { ControlValueAccessor, FormControl, NgControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Chain } from '../../models/Chain';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from "../../pipes/upperfirst-pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-chain-select',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
TranslatePipe,
|
||||
UpperfirstPipe
|
||||
],
|
||||
templateUrl: './chain-select.html',
|
||||
styleUrl: './chain-select.scss',
|
||||
})
|
||||
export class ChainSelect implements ControlValueAccessor, OnInit {
|
||||
private readonly chainDAO = inject(ChainDAO);
|
||||
protected chains$ = this.chainDAO.findAll();
|
||||
protected control = new FormControl();
|
||||
|
||||
constructor(@Self() @Optional() public controlDir: NgControl) {
|
||||
if (this.controlDir) {
|
||||
this.controlDir.valueAccessor = this;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.control = <FormControl>this.controlDir.control;
|
||||
}
|
||||
|
||||
writeValue(obj: any): void {}
|
||||
|
||||
registerOnChange(onChange: any) {}
|
||||
|
||||
registerOnTouched(fn: any): void {}
|
||||
|
||||
setDisabledState?(isDisabled: boolean): void {}
|
||||
|
||||
compareFn(c1: Chain, c2: Chain) {
|
||||
if (!c1 || !c2) return false;
|
||||
return c1.id === c2.id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EstablishmentAdd } from './establishment-add';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { Chain } from '../../models/Chain';
|
||||
import { Establishment } from '../../models/Establishment';
|
||||
import { EstablishmentSettings } from '../../services/establishment-settings';
|
||||
import { EstablishmentForm } from '../../pages/settings/establishments/establishment-form/establishment-form';
|
||||
|
||||
describe('EstablishmentAdd', () => {
|
||||
let component: EstablishmentAdd;
|
||||
let fixture: ComponentFixture<EstablishmentAdd>;
|
||||
|
||||
let establishmentSettings: Partial<EstablishmentSettings>;
|
||||
|
||||
beforeEach(async () => {
|
||||
establishmentSettings = {
|
||||
save: vi.fn(),
|
||||
};
|
||||
TestBed.overrideComponent(EstablishmentForm, {
|
||||
set: { template: `` },
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EstablishmentAdd],
|
||||
providers: [
|
||||
provideTranslateService(),
|
||||
{ provide: EstablishmentSettings, useValue: establishmentSettings },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EstablishmentAdd);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should save on valid form', async () => {
|
||||
const SELECTED_CHAIN_MOCK = new Chain('Mock', 'mock_logo.png', 1);
|
||||
const ADDRESS_MOCK = 'Mock street';
|
||||
await component.submit();
|
||||
//form is invalid so it doesnt call establishmenDAO save method
|
||||
expect(establishmentSettings.save).not.toHaveBeenCalled();
|
||||
//User chooses chain, form is valid
|
||||
component['form'].patchValue({ chain: SELECTED_CHAIN_MOCK, address: ADDRESS_MOCK });
|
||||
await component.submit();
|
||||
const establishment = new Establishment(SELECTED_CHAIN_MOCK, ADDRESS_MOCK);
|
||||
expect(establishmentSettings.save).toHaveBeenCalledExactlyOnceWith(establishment);
|
||||
});
|
||||
});
|
||||
36
src/app/components/establishment-add/establishment-add.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
|
||||
import { EstablishmentSettings } from '../../services/establishment-settings';
|
||||
import { Establishment } from '../../models/Establishment';
|
||||
import { ActionBtn } from '../action-btn/action-btn';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||
import { EstablishmentFormGroup } from '../../pages/settings/establishments/establishment-formgroup';
|
||||
import { SettingsBaseAddEdit } from '../settings-base-add-edit/settings-base-add-edit';
|
||||
|
||||
@Component({
|
||||
selector: 'app-establishment-add',
|
||||
imports: [
|
||||
ActionBtn,
|
||||
TranslatePipe,
|
||||
UpperfirstPipe,
|
||||
],
|
||||
templateUrl: './../settings-base-add-edit/settings-base-add-edit.html',
|
||||
styleUrl: './../settings-base-add-edit/settings-base-add-edit.scss',
|
||||
})
|
||||
export class EstablishmentAdd extends SettingsBaseAddEdit {
|
||||
private readonly establishmentSettings = inject(EstablishmentSettings);
|
||||
|
||||
readonly form = new EstablishmentFormGroup();
|
||||
btnText = 'common.save';
|
||||
title = 'settings.establishment.new_establishment';
|
||||
|
||||
async submit() {
|
||||
const chain = this.form.controls.chain.value;
|
||||
const address = this.form.controls.address.value;
|
||||
if (chain?.id) {
|
||||
const establishment = new Establishment(chain, address);
|
||||
this.establishmentSettings.save(establishment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EstablishmentEdit } from './establishment-edit';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Establishment } from '../../models/Establishment';
|
||||
import { Chain } from '../../models/Chain';
|
||||
import { EstablishmentSettings } from '../../services/establishment-settings';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { EstablishmentForm } from '../../pages/settings/establishments/establishment-form/establishment-form';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('EstablishmentEdit', () => {
|
||||
let component: EstablishmentEdit;
|
||||
let fixture: ComponentFixture<EstablishmentEdit>;
|
||||
|
||||
let activatedRoute: Partial<ActivatedRoute>;
|
||||
let establishmentSettings: Partial<EstablishmentSettings>;
|
||||
|
||||
const data = new BehaviorSubject({
|
||||
establishment: new Establishment(new Chain('mock', 'logo_mock.jpg', 1), 'mock street', 1),
|
||||
});
|
||||
beforeEach(async () => {
|
||||
activatedRoute = {
|
||||
data,
|
||||
};
|
||||
establishmentSettings = {
|
||||
update: vi.fn(),
|
||||
};
|
||||
|
||||
TestBed.overrideComponent(EstablishmentForm, {
|
||||
set: { template: `` },
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EstablishmentEdit],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: EstablishmentSettings, useValue: establishmentSettings },
|
||||
provideTranslateService(),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EstablishmentEdit);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call establishmentSettings update', async () => {
|
||||
const CHAIN_MOCK = new Chain('mock', 'logo_mock.png', 2);
|
||||
const ADDRESS_MOCK = 'mock street';
|
||||
//User selects chain
|
||||
component['form'].patchValue({chain: CHAIN_MOCK, address: ADDRESS_MOCK});
|
||||
const submitBtn = fixture.debugElement.query(By.css('app-action-btn'));
|
||||
submitBtn.triggerEventHandler('click');
|
||||
await fixture.whenStable()
|
||||
expect(establishmentSettings.update).toHaveBeenCalledExactlyOnceWith(new Establishment(CHAIN_MOCK, ADDRESS_MOCK, component['establishment']!.id));
|
||||
});
|
||||
});
|
||||
53
src/app/components/establishment-edit/establishment-edit.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { Establishment } from '../../models/Establishment';
|
||||
import { Observable, take, tap } from 'rxjs';
|
||||
import { EstablishmentFormGroup } from '../../pages/settings/establishments/establishment-formgroup';
|
||||
import { ActionBtn } from '../action-btn/action-btn';
|
||||
import { EstablishmentSettings } from '../../services/establishment-settings';
|
||||
import { SettingsBaseAddEdit } from '../settings-base-add-edit/settings-base-add-edit';
|
||||
|
||||
@Component({
|
||||
selector: 'app-establishment-edit',
|
||||
imports: [UpperfirstPipe, TranslatePipe, ActionBtn],
|
||||
templateUrl: './../settings-base-add-edit/settings-base-add-edit.html',
|
||||
styleUrl: './../settings-base-add-edit/settings-base-add-edit.scss',
|
||||
})
|
||||
export class EstablishmentEdit extends SettingsBaseAddEdit implements OnInit {
|
||||
private readonly activatedRoute = inject(ActivatedRoute);
|
||||
private readonly establishmentSettings = inject(EstablishmentSettings);
|
||||
|
||||
private establishment?: Establishment;
|
||||
readonly form = new EstablishmentFormGroup();
|
||||
btnText = 'common.update';
|
||||
title = 'settings.establishment.edit_establishment'
|
||||
|
||||
ngOnInit() {
|
||||
(<Observable<{ establishment: Establishment }>>this.activatedRoute.data)
|
||||
.pipe(
|
||||
take(1),
|
||||
tap((data) => (this.establishment = data.establishment)),
|
||||
tap((data) => this.patchForm(data.establishment)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
patchForm(establishment: Establishment) {
|
||||
this.form.patchValue({
|
||||
chain: establishment.chain,
|
||||
address: establishment.address,
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const chain = this.form.controls.chain.value;
|
||||
if (chain && this.establishment?.id) {
|
||||
const address = this.form.controls.address.value;
|
||||
const establishment = new Establishment(chain, address, this.establishment.id);
|
||||
await this.establishmentSettings.update(establishment);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<mat-toolbar>
|
||||
<button matIconButton (click)="bigClick.emit()">
|
||||
<mat-icon>{{icon()}}</mat-icon>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
23
src/app/components/floating-big-btn/floating-big-btn.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
:host {
|
||||
bottom: 80px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
mat-toolbar {
|
||||
button {
|
||||
background: var(--mat-sys-primary-container);
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
mat-icon {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
23
src/app/components/floating-big-btn/floating-big-btn.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FloatingBigBtn } from './floating-big-btn';
|
||||
|
||||
describe('FloatingBigBtn', () => {
|
||||
let component: FloatingBigBtn;
|
||||
let fixture: ComponentFixture<FloatingBigBtn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FloatingBigBtn]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FloatingBigBtn);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
15
src/app/components/floating-big-btn/floating-big-btn.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
|
||||
@Component({
|
||||
selector: 'app-floating-big-btn',
|
||||
imports: [MatToolbarModule, MatIcon, MatIconButton],
|
||||
templateUrl: './floating-big-btn.html',
|
||||
styleUrl: './floating-big-btn.scss',
|
||||
})
|
||||
export class FloatingBigBtn {
|
||||
readonly bigClick = output();
|
||||
readonly icon = input('add');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class IconActionListItem {
|
||||
constructor(public iconSrc: string, public label = '', public value: string){}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<mat-action-list>
|
||||
@for (item of items(); track item.value) {
|
||||
<button mat-list-item (click)="action.emit(item.value)">
|
||||
<img matListItemAvatar src="{{item.iconSrc}}" alt=""/>
|
||||
<span matListItemTitle>{{item.label|translate|upperfirst}}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-action-list>
|
||||
23
src/app/components/icon-action-list/icon-action-list.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IconActionList } from './icon-action-list';
|
||||
|
||||
describe('IconActionList', () => {
|
||||
let component: IconActionList;
|
||||
let fixture: ComponentFixture<IconActionList>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IconActionList]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(IconActionList);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
16
src/app/components/icon-action-list/icon-action-list.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { IconActionListItem } from './IconActionListItem';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from "../../pipes/upperfirst-pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-icon-action-list',
|
||||
imports: [MatListModule, TranslatePipe, UpperfirstPipe],
|
||||
templateUrl: './icon-action-list.html',
|
||||
styleUrl: './icon-action-list.scss',
|
||||
})
|
||||
export class IconActionList {
|
||||
readonly items = input<IconActionListItem[]>([]);
|
||||
readonly action = output<string>();
|
||||
}
|
||||
3
src/app/components/icon-nav-list/IconNavListItem.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export class IconNavListItem {
|
||||
constructor(public icon = 'thumb_up' , public label = '', public routerLink: string|string[] = ['/']){}
|
||||
}
|
||||
8
src/app/components/icon-nav-list/icon-nav-list.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<mat-nav-list>
|
||||
@for (item of items(); track item.label) {
|
||||
<a mat-list-item [routerLink]="item.routerLink">
|
||||
<mat-icon matListItemIcon>{{item.icon}}</mat-icon>
|
||||
<span matListItemTitle>{{item.label|translate|upperfirst }}</span>
|
||||
</a>
|
||||
}
|
||||
</mat-nav-list>
|
||||
0
src/app/components/icon-nav-list/icon-nav-list.scss
Normal file
23
src/app/components/icon-nav-list/icon-nav-list.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IconNavList } from './icon-nav-list';
|
||||
|
||||
describe('IconNavList', () => {
|
||||
let component: IconNavList;
|
||||
let fixture: ComponentFixture<IconNavList>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IconNavList]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(IconNavList);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
17
src/app/components/icon-nav-list/icon-nav-list.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { IconNavListItem } from './IconNavListItem';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from "../../pipes/upperfirst-pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-icon-nav-list',
|
||||
imports: [MatListModule, MatIcon, RouterLink, TranslatePipe, UpperfirstPipe],
|
||||
templateUrl: './icon-nav-list.html',
|
||||
styleUrl: './icon-nav-list.scss',
|
||||
})
|
||||
export class IconNavList {
|
||||
readonly items = input<IconNavListItem[]>([]);
|
||||
}
|
||||
20
src/app/components/image-uploader/image-uploader.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<input
|
||||
(change)="onFileSelected($event)"
|
||||
#fileUpload
|
||||
accept=".jpeg, .jpg, .png, .webp, .gif"
|
||||
class="file-input"
|
||||
type="file"
|
||||
/>
|
||||
<div class="file-upload">
|
||||
<div class="file-upload__description" title="{{ fileName() }}">
|
||||
{{ fileName() || 'common.no_file_yet' | translate | upperfirst }}
|
||||
</div>
|
||||
<button matMiniFab color="primary" (click)="fileUpload.click()">
|
||||
<mat-icon>attach_file</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@if (imgUrl()) {
|
||||
<div class="image-preview__container">
|
||||
<img [src]="imgUrl()" [alt]="fileName()" width="300" height="300" />
|
||||
</div>
|
||||
}
|
||||
27
src/app/components/image-uploader/image-uploader.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
.image-preview {
|
||||
&__container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
min-width: 196px;
|
||||
width: 100%;
|
||||
|
||||
&__description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
69
src/app/components/image-uploader/image-uploader.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ImageUploader } from './image-uploader';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { FiletypeUtils } from '../../services/filetype-utils';
|
||||
|
||||
describe('ImageUploader', () => {
|
||||
let component: ImageUploader;
|
||||
let fixture: ComponentFixture<ImageUploader>;
|
||||
|
||||
let filetypeUtils: Partial<FiletypeUtils>;
|
||||
|
||||
beforeEach(async () => {
|
||||
filetypeUtils = {
|
||||
isValidImageMimeType: vi.fn().mockReturnValue(true)
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ImageUploader],
|
||||
providers: [provideTranslateService(), { provide: FiletypeUtils, useValue: filetypeUtils }],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ImageUploader);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should update selection when fileIn input changes', async () => {
|
||||
const file = new File([''], 'test.png', { type: 'image/png' });
|
||||
fixture.componentRef.setInput('fileIn', file);
|
||||
fixture.detectChanges();
|
||||
const descriptionEl = fixture.debugElement.query(By.css('.file-upload__description'));
|
||||
expect((<HTMLDivElement>descriptionEl.nativeElement).textContent.toLowerCase().trim()).toEqual(
|
||||
file.name,
|
||||
);
|
||||
});
|
||||
|
||||
describe('onFileSelected', () => {
|
||||
it('should update and emit selection on valid file', () => {
|
||||
const emitSpy = vi.spyOn(component.file, 'emit');
|
||||
const file = new File([''], 'test.png', { type: 'image/png' });
|
||||
const inputEvent = { target: { files: [file] } } as any;
|
||||
component.onFileSelected(inputEvent);
|
||||
fixture.detectChanges();
|
||||
const descriptionEl = fixture.debugElement.query(By.css('.file-upload__description'));
|
||||
expect(
|
||||
(<HTMLDivElement>descriptionEl.nativeElement).textContent.toLowerCase().trim(),
|
||||
).toEqual(file.name);
|
||||
expect(emitSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should discard file if not valid type', () => {
|
||||
filetypeUtils.isValidImageMimeType = vi.fn().mockReturnValue(false);
|
||||
const file = new File([''], 'test.txt', { type: 'text/txt' });
|
||||
const inputEvent = { target: { files: [file] } } as any;
|
||||
component.onFileSelected(inputEvent);
|
||||
fixture.detectChanges();
|
||||
const descriptionEl = fixture.debugElement.query(By.css('.file-upload__description'));
|
||||
expect(
|
||||
(<HTMLDivElement>descriptionEl.nativeElement).textContent.toLowerCase().trim(),
|
||||
).not.toEqual(file.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
45
src/app/components/image-uploader/image-uploader.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Component, effect, inject, input, output, signal } from '@angular/core';
|
||||
import { MatMiniFabButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||
import { FiletypeUtils } from '../../services/filetype-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-image-uploader',
|
||||
imports: [MatIcon, MatMiniFabButton, TranslatePipe, UpperfirstPipe],
|
||||
templateUrl: './image-uploader.html',
|
||||
styleUrl: './image-uploader.scss',
|
||||
})
|
||||
export class ImageUploader {
|
||||
file = output<File>();
|
||||
fileIn = input<File | null>(null);
|
||||
protected readonly fileName = signal('');
|
||||
protected readonly imgUrl = signal('');
|
||||
private readonly filetypeUtils = inject(FiletypeUtils);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (this.fileIn()) {
|
||||
const file = <File>this.fileIn();
|
||||
this.updateSelection(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onFileSelected(e: Partial<Event>) {
|
||||
const files = (<HTMLInputElement>e.target).files;
|
||||
if (files) {
|
||||
const file = files[0];
|
||||
if (file && this.filetypeUtils.isValidImageMimeType(file.type)) {
|
||||
this.updateSelection(file);
|
||||
this.file.emit(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateSelection(file: File) {
|
||||
this.fileName.set(file.name);
|
||||
this.imgUrl.set(URL.createObjectURL(file));
|
||||
}
|
||||
}
|
||||
31
src/app/components/product-add/product-add.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProductAdd } from './product-add';
|
||||
import { ProductSettings } from '../../services/product-settings';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('ProductAdd', () => {
|
||||
let component: ProductAdd;
|
||||
let fixture: ComponentFixture<ProductAdd>;
|
||||
|
||||
let productSettings: Partial<ProductSettings>;
|
||||
|
||||
beforeEach(async () => {
|
||||
productSettings = {
|
||||
save: vi.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProductAdd],
|
||||
providers: [{ provide: ProductSettings, useValue: productSettings }, provideTranslateService()],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProductAdd);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
31
src/app/components/product-add/product-add.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { SettingsBaseAddEdit } from '../settings-base-add-edit/settings-base-add-edit';
|
||||
import { ProductFormGroup } from '../../pages/settings/products/product-formgroup';
|
||||
import { ActionBtn } from '../action-btn/action-btn';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||
import { ProductSettings } from '../../services/product-settings';
|
||||
import { Product } from '../../models/Product';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-add',
|
||||
imports: [ActionBtn, TranslatePipe, UpperfirstPipe],
|
||||
templateUrl: './../settings-base-add-edit/settings-base-add-edit.html',
|
||||
styleUrl: './../settings-base-add-edit/settings-base-add-edit.scss',
|
||||
})
|
||||
export class ProductAdd extends SettingsBaseAddEdit {
|
||||
btnText = 'common.save';
|
||||
title = 'settings.product.new_product';
|
||||
readonly form = new ProductFormGroup();
|
||||
|
||||
private readonly productSettings = inject(ProductSettings);
|
||||
|
||||
async submit() {
|
||||
const product = new Product(
|
||||
this.form.controls.barcode.value,
|
||||
this.form.controls.name.value,
|
||||
'',
|
||||
);
|
||||
await this.productSettings.save(product, this.form.controls.image.value);
|
||||
}
|
||||
}
|
||||
74
src/app/components/product-edit/product-edit.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProductEdit } from './product-edit';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Product } from '../../models/Product';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ImageStorage } from '../../services/image-storage';
|
||||
import { ProductSettings } from '../../services/product-settings';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('ProductEdit', () => {
|
||||
let component: ProductEdit;
|
||||
let fixture: ComponentFixture<ProductEdit>;
|
||||
|
||||
let activatedRoute: Partial<ActivatedRoute>;
|
||||
let imageStorage: Partial<ImageStorage>;
|
||||
let productSettings: Partial<ProductSettings>;
|
||||
|
||||
const dataSubject = new BehaviorSubject({ product: new Product('mock', 'mock', 'mock.jpg', 1) });
|
||||
|
||||
beforeEach(async () => {
|
||||
activatedRoute = {
|
||||
data: dataSubject,
|
||||
};
|
||||
imageStorage = {
|
||||
getImage: vi.fn().mockResolvedValue(new Blob([], { type: 'image/png' })),
|
||||
};
|
||||
productSettings = {
|
||||
update: vi.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProductEdit],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: ImageStorage, useValue: imageStorage },
|
||||
{ provide: ProductSettings, useValue: productSettings },
|
||||
provideTranslateService(),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProductEdit);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should fetch image from imageStorage', () => {
|
||||
expect(imageStorage.getImage).toHaveBeenCalledTimes(1);
|
||||
|
||||
});
|
||||
|
||||
//TODO: there is some sync issue by using a getter for the disabled state
|
||||
it('should not call update on update btn click if form is unchanged', () => {
|
||||
expect(component.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should call productSettings update on product update', async () => {
|
||||
//User updates the product name
|
||||
component.form.controls.name.patchValue('updated name mock');
|
||||
component.form.controls.name.markAsDirty();
|
||||
await fixture.whenStable();
|
||||
expect(component.disabled).toBe(false);
|
||||
const updateBtn = fixture.debugElement.query(By.css('app-action-btn'));
|
||||
//this would trigger the update anyway thats why we check for the button not to be disabled
|
||||
updateBtn.triggerEventHandler('click');
|
||||
expect(productSettings.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
});
|
||||
71
src/app/components/product-edit/product-edit.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { SettingsBaseAddEdit } from '../settings-base-add-edit/settings-base-add-edit';
|
||||
import { ProductFormGroup } from '../../pages/settings/products/product-formgroup';
|
||||
import { ActionBtn } from '../action-btn/action-btn';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable, take, tap } from 'rxjs';
|
||||
import { Product } from '../../models/Product';
|
||||
import { ImageStorage } from '../../services/image-storage';
|
||||
import { ProductSettings } from '../../services/product-settings';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-edit',
|
||||
imports: [ActionBtn, TranslatePipe, UpperfirstPipe],
|
||||
templateUrl: './../settings-base-add-edit/settings-base-add-edit.html',
|
||||
styleUrl: './../settings-base-add-edit/settings-base-add-edit.scss',
|
||||
})
|
||||
export class ProductEdit extends SettingsBaseAddEdit implements OnInit {
|
||||
btnText = 'common.update';
|
||||
title = 'settings.product.edit_product';
|
||||
readonly form = new ProductFormGroup();
|
||||
private readonly activatedRoute = inject(ActivatedRoute);
|
||||
private readonly imageStorage = inject(ImageStorage);
|
||||
private product?: Product;
|
||||
private readonly productSettings = inject(ProductSettings);
|
||||
|
||||
ngOnInit() {
|
||||
(<Observable<{ product: Product }>>this.activatedRoute.data)
|
||||
.pipe(
|
||||
take(1),
|
||||
tap(
|
||||
(data) =>
|
||||
(this.product = new Product(
|
||||
data.product.barcode,
|
||||
data.product.name,
|
||||
data.product.image,
|
||||
data.product.id,
|
||||
)),
|
||||
),
|
||||
tap((data) => this.patchForm(data.product)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async patchForm(product: Product) {
|
||||
try {
|
||||
this.form.controls.barcode.patchValue(product.barcode);
|
||||
this.form.controls.name.patchValue(product.name);
|
||||
if (product.image) {
|
||||
const imgName = product.image;
|
||||
const blob = await this.imageStorage.getImage(imgName);
|
||||
const file = new File([blob], imgName, { type: blob.type });
|
||||
this.form.controls.image.patchValue(file);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//TODO: reportar error
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.product) {
|
||||
const updatedBarcode = this.form.controls.barcode.value;
|
||||
const updatedName = this.form.controls.name.value;
|
||||
const updatedImg = this.form.controls.image.value;
|
||||
const updatedProduct = new Product(updatedBarcode, updatedName, '', this.product.id);
|
||||
await this.productSettings.update(this.product, updatedProduct, updatedImg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<scan-loading-screen></scan-loading-screen>
|
||||
@@ -0,0 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ScanLoadingScreen } from './scan-loading-screen';
|
||||
|
||||
describe('ScanLoadingScreen', () => {
|
||||
let component: ScanLoadingScreen;
|
||||
let fixture: ComponentFixture<ScanLoadingScreen>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ScanLoadingScreen]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ScanLoadingScreen);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scan-loading-screen',
|
||||
imports: [],
|
||||
templateUrl: './scan-loading-screen.html',
|
||||
styleUrl: './scan-loading-screen.scss',
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class ScanLoadingScreen {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<h3>{{title|translate|upperfirst}}</h3>
|
||||
<ng-content></ng-content>
|
||||
<app-action-btn
|
||||
(click)="submit()"
|
||||
(keydown)="submit()"
|
||||
[disabled]="this.disabled"
|
||||
class="top-auto"
|
||||
text="{{btnText|translate|upperfirst}}"
|
||||
/>
|
||||
@@ -0,0 +1,9 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
export abstract class SettingsBaseAddEdit {
|
||||
abstract btnText: string;
|
||||
abstract title: string;
|
||||
abstract form: FormGroup;
|
||||
|
||||
protected abstract submit(): Promise<void>
|
||||
|
||||
get disabled() {
|
||||
return this.form.invalid || this.form.pristine;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<app-simple-list-w-actions [items]="(data$|async) ?? []" (action)="edit($event)"/>
|
||||
<app-floating-big-btn icon="add" (bigClick)="add()"/>
|
||||
@@ -0,0 +1,5 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { SimpleListItem } from '../simple-list-w-actions/SimpleListItem';
|
||||
|
||||
export abstract class SettingsBaseList {
|
||||
abstract data$: Observable<SimpleListItem[]>;
|
||||
|
||||
protected abstract edit(action: { action: string; subject: string }): void;
|
||||
protected abstract add(): void;
|
||||
}
|
||||
11
src/app/components/simple-layout/simple-layout.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="content">
|
||||
<header>
|
||||
@if(withBackBtn()){
|
||||
<button class="icon" (click)="location.back()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<h1>{{title()|translate|upperfirst}}</h1>
|
||||
</header>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
19
src/app/components/simple-layout/simple-layout.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
header {
|
||||
display: flex;
|
||||
button {
|
||||
background-color: unset;
|
||||
border: unset;
|
||||
}
|
||||
& .icon {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0px 16px;
|
||||
}
|
||||
25
src/app/components/simple-layout/simple-layout.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SimpleLayout } from './simple-layout';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('SimpleLayout', () => {
|
||||
let component: SimpleLayout;
|
||||
let fixture: ComponentFixture<SimpleLayout>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SimpleLayout],
|
||||
providers: [provideTranslateService({})],
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SimpleLayout);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
16
src/app/components/simple-layout/simple-layout.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Component, inject, input } from '@angular/core';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { MatIcon } from "@angular/material/icon";
|
||||
import { UpperfirstPipe } from "../../pipes/upperfirst-pipe";
|
||||
@Component({
|
||||
selector: 'app-simple-layout',
|
||||
imports: [TranslatePipe, MatIcon, UpperfirstPipe],
|
||||
templateUrl: './simple-layout.html',
|
||||
styleUrl: './simple-layout.scss',
|
||||
})
|
||||
export class SimpleLayout {
|
||||
protected location = inject(Location)
|
||||
readonly title = input('');
|
||||
readonly withBackBtn = input(false);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SimpleListItemAction } from "./SimpleListItemAction";
|
||||
|
||||
export class SimpleListItem {
|
||||
constructor(public id: string, public text: string = '', public actions: SimpleListItemAction[] = []) {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class SimpleListItemAction {
|
||||
constructor(public icon: string, public action: string) {}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<mat-list>
|
||||
@for (item of items(); track item.id) {
|
||||
<mat-list-item>
|
||||
{{ item.text }}
|
||||
@if (item.actions; as actions) {
|
||||
<div matListItemMeta>
|
||||
@for (action of actions; track action.action) {
|
||||
<button matIconButton (click)="this.action.emit({action: action.action, subject: item.id})">
|
||||
<mat-icon>{{ action.icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
</mat-list>
|
||||