Compare commits

..

97 Commits

Author SHA1 Message Date
79ce5eadbb build: prevent mangling on prod bundle (temp fix) 2026-02-22 23:46:23 -03:00
82dd3c607a feat(product): add crate and edit ops in settings 2026-02-22 23:35:30 -03:00
f89a27320f feat(products): add resolvers 2026-02-22 23:33:08 -03:00
564e735b14 test: extends coverage 2026-02-22 22:49:20 -03:00
ec5055e8d6 refactor: chain form to use handleImage directive 2026-02-22 20:07:03 -03:00
5050da9936 feat: add directive for handling images in form 2026-02-22 20:04:46 -03:00
d25b7cb49c fix: mark input as dirty after patching value 2026-02-22 15:52:23 -03:00
9b3e42a161 feat: add input with scan capability for barcodes 2026-02-19 21:44:49 -03:00
96e3854945 chore(i18n): add new words 2026-02-19 21:40:01 -03:00
bab5f6fd15 fix: prevent the component showing on start 2026-02-19 20:46:32 -03:00
0e8fe172b5 refactor: switch image place from top to bottom 2026-02-18 22:53:10 -03:00
105514423f fix: css styles to keep the scanner on top of content 2026-02-18 21:43:10 -03:00
5d6a11e253 fix: remove unused css class 2026-02-18 20:58:32 -03:00
2d4805228a fix: fix mapping 2026-02-17 20:58:58 -03:00
63679b5087 refactor: not to drop all tables on app init 2026-02-17 19:53:17 -03:00
eef3dbc2fb feat: add establishment settings 2026-02-17 19:47:25 -03:00
816252308c refactor: chain list to extend base settings list 2026-02-17 19:32:36 -03:00
36df70bf38 feat: add base class for settings listing 2026-02-17 19:27:45 -03:00
91c09a0ca3 refactor: edit chain to extend settings base add edit class 2026-02-17 18:48:46 -03:00
0d49ee6dd8 refactor: trigger cd on image valueChanges 2026-02-17 18:42:57 -03:00
3c87de3d51 refactor: extend base settings add class to edit 2026-02-17 01:36:12 -03:00
736a658323 refactor: base members to be abstract as default values are senseless 2026-02-16 22:38:48 -03:00
f140ef403b refactor: add chain to extend settings base add 2026-02-16 22:28:00 -03:00
13b19d5776 feat: add base class for settings add screen 2026-02-16 21:46:25 -03:00
4e53684649 refactor: remove unneeded imports 2026-02-16 17:43:21 -03:00
836c6652ec refactor: always navigate, rm edit/add scren from history stack 2026-02-16 17:25:58 -03:00
7de993a765 refactor: add service for chains settings 2026-02-14 00:28:41 -03:00
6babbea1c4 feat: add chain select cmp 2026-02-11 21:19:55 -03:00
5d4c0c5d36 refactor: wip app 2026-02-08 00:00:20 -03:00
3ee45adaa6 feat: add home page 2026-02-07 23:59:26 -03:00
267f660512 feat: add routing for settings 2026-02-07 23:55:30 -03:00
454f93fb11 feat: add settings page 2026-02-07 23:52:02 -03:00
e7c65dd268 feat: add chain management components 2026-02-07 23:47:12 -03:00
73bf96f1ef feat: resolvers for chain edit and list 2026-02-07 23:43:12 -03:00
e827a14284 feat: chains form 2026-02-07 23:40:47 -03:00
1146bf6f8d feat: chain form group 2026-02-07 23:32:23 -03:00
d4723e6a24 refactor: await rejections on tests 2026-02-07 23:27:20 -03:00
b07bc1db30 feat: language selection 2026-02-07 23:24:02 -03:00
010646da8a feat: action btn 2026-02-07 23:17:53 -03:00
9b62766c66 chore: assets for language selection 2026-02-07 23:05:27 -03:00
2c172dd3d1 feat: add custom file input for images 2026-02-07 23:00:12 -03:00
e803c670f4 feat: service to store images with OPFS 2026-02-06 21:41:20 -03:00
c0a3da4635 feat: add utility service for handling file types 2026-02-05 22:45:46 -03:00
2c332c6758 fix: update test to work with native browser 2026-02-04 21:47:22 -03:00
ea2779c681 fix: add coverage to vitest config 2026-02-04 21:41:45 -03:00
f89fe2e323 test: switch runner to browser mode
- Run tests in native browser instead of using jsdom
2026-02-04 21:25:34 -03:00
1502418cfa feat: add origin private file system service 2026-02-03 22:28:21 -03:00
1bf0b71ca7 fix: log error as error 2026-02-01 20:42:38 -03:00
d44ba2b421 chore(i18n): extends dictionaries 2026-01-31 22:58:12 -03:00
f06d5396ad style: add utility classes, apply borderbox to all elements 2026-01-31 22:35:24 -03:00
2e77ab3e40 style: add outlined icons font 2026-01-31 22:34:03 -03:00
9a3dd87896 build: prevent mangling (minifying classes and functions names) on build 2026-01-31 22:30:56 -03:00
2e6d0c1f0a refactor: change titlecase to upperfirst for capitalizing 2026-01-31 22:29:03 -03:00
6615912a35 feat: add pipe that capitalize first word only 2026-01-31 22:25:32 -03:00
1723ea7a39 refactor: height 100% and back btn 2026-01-31 11:30:15 -03:00
ae249ea828 feat: add list with actions component 2026-01-29 20:57:26 -03:00
4397e5fec3 refactor: moves positioning to host 2026-01-26 23:45:37 -03:00
f34ffec5b0 fix: bigClick to emit on click 2026-01-26 23:01:52 -03:00
a4aba009b8 fix: properly aligns button 2026-01-26 22:54:35 -03:00
7208a08ffb feat: add floating btn 2026-01-26 21:08:14 -03:00
85da313957 feat: add simplet layout for pages 2026-01-25 16:03:05 -03:00
3774750a56 feat: add some listing components 2026-01-25 16:02:33 -03:00
68866f4725 feat: navigation bar 2026-01-24 19:26:29 -03:00
00209ce137 build: add i18n support 2026-01-20 22:54:08 -03:00
490b929fa9 refactor: clean app component 2026-01-20 20:42:11 -03:00
94eb9b1bf8 build: ad material 2026-01-20 20:29:50 -03:00
c5e6ccc4c8 refactor: remove pwa bg color 2026-01-18 21:55:13 -03:00
9c6750617b chore: update favicon 2026-01-18 21:09:13 -03:00
3f4edb954c build: add pwa capability 2026-01-18 21:05:26 -03:00
dfacc39e57 test: add unit tests for DAOs 2026-01-18 18:36:34 -03:00
e125fb6e64 build: excludes html files from test coverage report 2026-01-18 17:59:59 -03:00
0a56fc6be0 fix: removes console.log 2026-01-18 15:13:05 -03:00
3467a574c1 fix: remove console.log 2026-01-18 00:32:28 -03:00
adc20904cc test(model/db): test instantiation 2026-01-17 22:56:19 -03:00
af84abe3f8 test(models/domain): test instantiation 2026-01-17 22:46:53 -03:00
4c51822bf0 test: extends coverage for sqlite service 2026-01-17 22:16:11 -03:00
8e53710c90 build: add testing coverage tool for vitest 2026-01-17 19:03:12 -03:00
4655ba4f64 feat(barcode reader): add wrapper for the webcomponent 2026-01-17 18:54:36 -03:00
ead327d44f feat(barcode reader): add close btn 2026-01-17 18:51:37 -03:00
a8ff28829d fix: increment z-index 2026-01-17 18:24:29 -03:00
888d636be6 refactor: db initialization 2026-01-17 16:05:22 -03:00
230ba670ab feat(persistance): add migration 2026-01-17 15:01:49 -03:00
fd763cc162 feat(persistance) : add DAO 2026-01-17 15:01:16 -03:00
8b462f98f6 feat(persistance): add db models 2026-01-17 15:00:24 -03:00
90233c7208 feat: add domain models 2026-01-17 14:58:39 -03:00
ab2aca1d97 feat(persistance): add additional ops for sqlite 2026-01-17 14:43:40 -03:00
fcee6e0b19 test: remove outdated test 2026-01-03 00:21:16 -03:00
5e1e453b0b feat(loading screen): add angular wrapper 2026-01-03 00:20:02 -03:00
a870e875c8 test: add missing dependency 2026-01-03 00:12:23 -03:00
49b9389532 feat: add barcode reader 2026-01-02 23:32:01 -03:00
7c6045f1b3 refactor: replace scan loading style loading 2026-01-02 22:42:05 -03:00
7952e5632a refactor: add loading screen on app initialization 2026-01-01 22:33:18 -03:00
92e426b316 feat(loading): add loading screen as web component 2026-01-01 22:14:58 -03:00
f095efd9e0 refactor: add global styles 2026-01-01 21:45:18 -03:00
1290130d8e feat(wa-sqlite): db initiallization 2026-01-01 14:00:30 -03:00
7be44bd8f2 feat(wa-sqlite): add mock cert, modifies npm dev command 2026-01-01 13:11:47 -03:00
0938dc861d build: add wrapped wa-sqlite for angular 2025-12-27 20:14:38 -03:00
237 changed files with 7564 additions and 413 deletions

View File

@@ -27,6 +27,16 @@
{ {
"glob": "**/*", "glob": "**/*",
"input": "public" "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": [ "styles": [
@@ -38,8 +48,8 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "500kB", "maximumWarning": "2MB",
"maximumError": "1MB" "maximumError": "4MB"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
@@ -47,7 +57,8 @@
"maximumError": "8kB" "maximumError": "8kB"
} }
], ],
"outputHashing": "all" "outputHashing": "all",
"serviceWorker": "ngsw-config.json"
}, },
"development": { "development": {
"optimization": false, "optimization": false,
@@ -67,10 +78,20 @@
"buildTarget": "groceries-price-tracker:build:development" "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": { "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
View 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
View 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
View 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
View File

@@ -8,12 +8,18 @@
"name": "groceries-price-tracker", "name": "groceries-price-tracker",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@angular/cdk": "^21.1.0",
"@angular/common": "^21.0.0", "@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0", "@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0", "@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0", "@angular/forms": "^21.0.0",
"@angular/material": "^21.1.0",
"@angular/platform-browser": "^21.0.0", "@angular/platform-browser": "^21.0.0",
"@angular/router": "^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", "rxjs": "~7.8.0",
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -21,6 +27,8 @@
"@angular/build": "^21.0.4", "@angular/build": "^21.0.4",
"@angular/cli": "^21.0.4", "@angular/cli": "^21.0.4",
"@angular/compiler-cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0",
"@vitest/browser-playwright": "^4.0.17",
"@vitest/coverage-v8": "^4.0.17",
"jsdom": "^27.1.0", "jsdom": "^27.1.0",
"typescript": "~5.9.2", "typescript": "~5.9.2",
"vitest": "^4.0.8" "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": { "node_modules/@angular/cli": {
"version": "21.0.4", "version": "21.0.4",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.4.tgz", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.4.tgz",
@@ -560,6 +584,23 @@
"rxjs": "^6.5.3 || ^7.4.0" "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": { "node_modules/@angular/platform-browser": {
"version": "21.0.6", "version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.6.tgz", "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" "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": { "node_modules/@asamuzakjp/css-color": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz",
@@ -948,6 +1008,16 @@
"node": ">=6.9.0" "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": { "node_modules/@csstools/color-helpers": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", "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": { "node_modules/@isaacs/balanced-match": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@@ -2596,6 +2701,32 @@
"@tybys/wasm-util": "^0.10.1" "@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": { "node_modules/@npmcli/agent": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz",
@@ -3239,6 +3370,13 @@
"license": "MIT", "license": "MIT",
"optional": true "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": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-beta.47", "version": "1.0.0-beta.47",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.47.tgz", "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": "^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": { "node_modules/@standard-schema/spec": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT" "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": { "node_modules/@tufjs/canonical-json": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", "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" "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": { "node_modules/@vitest/expect": {
"version": "4.0.16", "version": "4.0.17",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz",
"integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", "integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@standard-schema/spec": "^1.0.0", "@standard-schema/spec": "^1.0.0",
"@types/chai": "^5.2.2", "@types/chai": "^5.2.2",
"@vitest/spy": "4.0.16", "@vitest/spy": "4.0.17",
"@vitest/utils": "4.0.16", "@vitest/utils": "4.0.17",
"chai": "^6.2.1", "chai": "^6.2.1",
"tinyrainbow": "^3.0.3" "tinyrainbow": "^3.0.3"
}, },
@@ -3997,13 +4256,13 @@
} }
}, },
"node_modules/@vitest/mocker": { "node_modules/@vitest/mocker": {
"version": "4.0.16", "version": "4.0.17",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz",
"integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", "integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/spy": "4.0.16", "@vitest/spy": "4.0.17",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"magic-string": "^0.30.21" "magic-string": "^0.30.21"
}, },
@@ -4034,9 +4293,9 @@
} }
}, },
"node_modules/@vitest/pretty-format": { "node_modules/@vitest/pretty-format": {
"version": "4.0.16", "version": "4.0.17",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz",
"integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", "integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4047,13 +4306,13 @@
} }
}, },
"node_modules/@vitest/runner": { "node_modules/@vitest/runner": {
"version": "4.0.16", "version": "4.0.17",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz",
"integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", "integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/utils": "4.0.16", "@vitest/utils": "4.0.17",
"pathe": "^2.0.3" "pathe": "^2.0.3"
}, },
"funding": { "funding": {
@@ -4061,13 +4320,13 @@
} }
}, },
"node_modules/@vitest/snapshot": { "node_modules/@vitest/snapshot": {
"version": "4.0.16", "version": "4.0.17",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz",
"integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", "integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/pretty-format": "4.0.16", "@vitest/pretty-format": "4.0.17",
"magic-string": "^0.30.21", "magic-string": "^0.30.21",
"pathe": "^2.0.3" "pathe": "^2.0.3"
}, },
@@ -4086,9 +4345,9 @@
} }
}, },
"node_modules/@vitest/spy": { "node_modules/@vitest/spy": {
"version": "4.0.16", "version": "4.0.17",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz",
"integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", "integrity": "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -4096,13 +4355,13 @@
} }
}, },
"node_modules/@vitest/utils": { "node_modules/@vitest/utils": {
"version": "4.0.16", "version": "4.0.17",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.17.tgz",
"integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", "integrity": "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/pretty-format": "4.0.16", "@vitest/pretty-format": "4.0.17",
"tinyrainbow": "^3.0.3" "tinyrainbow": "^3.0.3"
}, },
"funding": { "funding": {
@@ -4211,6 +4470,22 @@
"node": ">= 14.0.0" "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": { "node_modules/ansi-escapes": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
@@ -4263,6 +4538,25 @@
"node": ">=12" "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": { "node_modules/baseline-browser-mapping": {
"version": "2.9.11", "version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
@@ -5539,6 +5833,16 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "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": "^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": { "node_modules/htmlparser2": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
@@ -5754,6 +6065,16 @@
"node": "^18.17.0 || >=20.5.0" "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": { "node_modules/ip-address": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
@@ -5916,6 +6237,35 @@
"node": ">=10" "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": { "node_modules/jose": {
"version": "6.1.3", "version": "6.1.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
@@ -6020,7 +6370,6 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/jsonparse": { "node_modules/jsonparse": {
@@ -6208,6 +6557,34 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@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": { "node_modules/make-fetch-happen": {
"version": "15.0.3", "version": "15.0.3",
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz",
@@ -7029,7 +7406,6 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"entities": "^6.0.0" "entities": "^6.0.0"
@@ -7083,7 +7459,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
@@ -7197,6 +7572,19 @@
"@napi-rs/nice": "^1.0.4" "@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": { "node_modules/pkce-challenge": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
@@ -7207,6 +7595,50 @@
"node": ">=16.20.0" "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": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -7765,6 +8197,21 @@
"node": "^20.17.0 || >=22.9.0" "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": { "node_modules/slice-ansi": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", "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" "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": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "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": ">=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": { "node_modules/tough-cookie": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
@@ -8862,19 +9332,19 @@
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
"version": "4.0.16", "version": "4.0.17",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.17.tgz",
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/expect": "4.0.16", "@vitest/expect": "4.0.17",
"@vitest/mocker": "4.0.16", "@vitest/mocker": "4.0.17",
"@vitest/pretty-format": "4.0.16", "@vitest/pretty-format": "4.0.17",
"@vitest/runner": "4.0.16", "@vitest/runner": "4.0.17",
"@vitest/snapshot": "4.0.16", "@vitest/snapshot": "4.0.17",
"@vitest/spy": "4.0.16", "@vitest/spy": "4.0.17",
"@vitest/utils": "4.0.16", "@vitest/utils": "4.0.17",
"es-module-lexer": "^1.7.0", "es-module-lexer": "^1.7.0",
"expect-type": "^1.2.2", "expect-type": "^1.2.2",
"magic-string": "^0.30.21", "magic-string": "^0.30.21",
@@ -8902,10 +9372,10 @@
"@edge-runtime/vm": "*", "@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.0.16", "@vitest/browser-playwright": "4.0.17",
"@vitest/browser-preview": "4.0.16", "@vitest/browser-preview": "4.0.17",
"@vitest/browser-webdriverio": "4.0.16", "@vitest/browser-webdriverio": "4.0.17",
"@vitest/ui": "4.0.16", "@vitest/ui": "4.0.17",
"happy-dom": "*", "happy-dom": "*",
"jsdom": "*" "jsdom": "*"
}, },
@@ -9291,6 +9761,13 @@
"peerDependencies": { "peerDependencies": {
"zod": "^3.25 || ^4" "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
} }
} }
} }

View File

@@ -3,8 +3,9 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve --host 0.0.0.0 --ssl true --ssl-key \"./cert/server.key\" --ssl-cert \"./cert/server.crt\"",
"build": "ng build", "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", "watch": "ng build --watch --configuration development",
"test": "ng test" "test": "ng test"
}, },
@@ -23,12 +24,18 @@
"private": true, "private": true,
"packageManager": "npm@11.5.1", "packageManager": "npm@11.5.1",
"dependencies": { "dependencies": {
"@angular/cdk": "^21.1.0",
"@angular/common": "^21.0.0", "@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0", "@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0", "@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0", "@angular/forms": "^21.0.0",
"@angular/material": "^21.1.0",
"@angular/platform-browser": "^21.0.0", "@angular/platform-browser": "^21.0.0",
"@angular/router": "^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", "rxjs": "~7.8.0",
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -36,6 +43,8 @@
"@angular/build": "^21.0.4", "@angular/build": "^21.0.4",
"@angular/cli": "^21.0.4", "@angular/cli": "^21.0.4",
"@angular/compiler-cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0",
"@vitest/browser-playwright": "^4.0.17",
"@vitest/coverage-v8": "^4.0.17",
"jsdom": "^27.1.0", "jsdom": "^27.1.0",
"typescript": "~5.9.2", "typescript": "~5.9.2",
"vitest": "^4.0.8" "vitest": "^4.0.8"

View 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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

49
public/i18n/en.json Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/icons/flags/sp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

BIN
public/icons/flags/us.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/icons/icon-48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
public/icons/icon-72x72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
public/icons/icon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View 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"
}
]
}

File diff suppressed because it is too large Load Diff

View 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 { provideRouter } from '@angular/router';
import { routes } from './app.routes'; 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 = { export const appConfig: ApplicationConfig = {
providers: [ 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(), 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',
}),
],
}; };

View File

@@ -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="content">
<div class="left-side"> <router-outlet></router-outlet>
<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>
</div> </div>
<div class="divider" role="separator" aria-label="Divider"></div> <app-bottom-navigation-bar [options]="menuOptions"/>
<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 />

View File

@@ -1,3 +1,30 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { Home } from './pages/home/home';
export const routes: Routes = []; 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',
},
];

View File

@@ -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;
}

View File

@@ -1,10 +1,13 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { App } from './app'; import { App } from './app';
import { provideTranslateService } from '@ngx-translate/core';
describe('App', () => { describe('App', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [App], imports: [App],
providers: [provideTranslateService({})],
}).compileComponents(); }).compileComponents();
}); });
@@ -13,11 +16,4 @@ describe('App', () => {
const app = fixture.componentInstance; const app = fixture.componentInstance;
expect(app).toBeTruthy(); 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');
});
}); });

View File

@@ -1,12 +1,33 @@
import { Component, signal } from '@angular/core'; import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { BottomNavigationBar } from './components/bottom-navigation-bar/bottom-navigation-bar';
import { BottomNavigationBarOption } from './components/bottom-navigation-bar/BottomNavigationBarOption';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet], imports: [RouterOutlet, BottomNavigationBar],
templateUrl: './app.html', templateUrl: './app.html',
styleUrl: './app.scss' styleUrls: [
'./app.scss'
],
}) })
export class App { 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'),
];
} }

View File

@@ -0,0 +1,3 @@
<div class="action-btn">
<button matButton="outlined" [disabled]="disabled()">{{text()}}</button>
</div>

View File

@@ -0,0 +1,12 @@
:host {
pointer-events: none;
}
.action-btn {
padding: 24px 0;
width: 100%;
button {
width: 100%;
pointer-events: all;
}
}

View 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();
});
});

View 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('');
}

View 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>

View 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%;
}

View 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();
});
});
});

View 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 {}
}

View 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()"
/>

View File

@@ -0,0 +1,8 @@
barcode-reader {
display: block;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 999;
}

View 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();
});
});

View 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';

View File

@@ -0,0 +1,8 @@
export class BottomNavigationBarOption {
constructor(
public readonly label: string,
public readonly icon: string,
public readonly path: string,
public readonly fontSet: string = '',
) {}
}

View File

@@ -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>

View File

@@ -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;
}
}
}
}

View File

@@ -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();
});
});

View File

@@ -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[]>([]);
}

View 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,
);
});
});

View 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);
}
}

View 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);
});
});

View 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);
}
}
}

View 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>

View File

@@ -0,0 +1,3 @@
mat-form-field {
width: 100%;
}

View 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]);
});
});

View 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;
}
}

View File

@@ -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);
});
});

View 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);
}
}
}

View File

@@ -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));
});
});

View 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);
}
}
}

View File

@@ -0,0 +1,5 @@
<mat-toolbar>
<button matIconButton (click)="bigClick.emit()">
<mat-icon>{{icon()}}</mat-icon>
</button>
</mat-toolbar>

View 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;
}
}

View 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();
});
});

View 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');
}

View File

@@ -0,0 +1,3 @@
export class IconActionListItem {
constructor(public iconSrc: string, public label = '', public value: string){}
}

View File

@@ -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>

View 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();
});
});

View 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>();
}

View File

@@ -0,0 +1,3 @@
export class IconNavListItem {
constructor(public icon = 'thumb_up' , public label = '', public routerLink: string|string[] = ['/']){}
}

View 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>

View 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();
});
});

View 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[]>([]);
}

View 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>
}

View 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;
}
}

View 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);
});
});
});

View 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));
}
}

View 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();
});
});

View 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);
}
}

View 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);
});
});

View 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);
}
}
}

View File

@@ -0,0 +1 @@
<scan-loading-screen></scan-loading-screen>

View File

@@ -0,0 +1,7 @@
:host {
display: block;
height: 100%;
position: absolute;
width: 100%;
z-index: 999;
}

View File

@@ -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();
});
});

View File

@@ -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 {
}

View File

@@ -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}}"
/>

View File

@@ -0,0 +1,9 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
}
h3 {
margin-top: 0;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,2 @@
<app-simple-list-w-actions [items]="(data$|async) ?? []" (action)="edit($event)"/>
<app-floating-big-btn icon="add" (bigClick)="add()"/>

View File

@@ -0,0 +1,5 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
}

View File

@@ -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;
}

View 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>

View 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;
}

View 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();
});
});

View 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);
}

View File

@@ -0,0 +1,5 @@
import { SimpleListItemAction } from "./SimpleListItemAction";
export class SimpleListItem {
constructor(public id: string, public text: string = '', public actions: SimpleListItemAction[] = []) {}
}

View File

@@ -0,0 +1,3 @@
export class SimpleListItemAction {
constructor(public icon: string, public action: string) {}
}

View File

@@ -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>

Some files were not shown because too many files have changed in this diff Show More