diff --git a/frontend/angular.json b/frontend/angular.json index 19d7b2e..d8f2f0c 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -67,7 +67,8 @@ "maximumError": "8kB" } ], - "outputHashing": "all" + "outputHashing": "all", + "serviceWorker": "ngsw-config.json" }, "development": { "optimization": false, diff --git a/frontend/ngsw-config.json b/frontend/ngsw-config.json new file mode 100644 index 0000000..69edd28 --- /dev/null +++ b/frontend/ngsw-config.json @@ -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)" + ] + } + } + ] +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fc0d2d0..239d8ff 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@angular/platform-browser": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.0", "@angular/router": "^19.2.0", + "@angular/service-worker": "^19.2.0", "@fontsource/fredoka": "^5.2.10", "@fontsource/nunito": "^5.2.7", "@fontsource/opendyslexic": "^5.2.5", @@ -1185,6 +1186,25 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/service-worker": { + "version": "19.2.25", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-19.2.25.tgz", + "integrity": "sha512-NB4g5bA+f5Jx6UsLmEdrHUXLOQDtAjQqeM7JHRl7qXg/xESnNubNqM0VMKO1P5oqAjQ++5Vc1G3s59uOgHKnPw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "bin": { + "ngsw-config": "ngsw-config.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.2.25", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -14741,474 +14761,6 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, "node_modules/vite/node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", diff --git a/frontend/package.json b/frontend/package.json index e451f80..0c9f944 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@angular/platform-browser": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.0", "@angular/router": "^19.2.0", + "@angular/service-worker": "^19.2.0", "@fontsource/fredoka": "^5.2.10", "@fontsource/nunito": "^5.2.7", "@fontsource/opendyslexic": "^5.2.5", @@ -37,4 +38,4 @@ "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.7.2" } -} +} \ No newline at end of file diff --git a/frontend/public/icons/icon-128x128.png b/frontend/public/icons/icon-128x128.png new file mode 100644 index 0000000..c2573b9 Binary files /dev/null and b/frontend/public/icons/icon-128x128.png differ diff --git a/frontend/public/icons/icon-144x144.png b/frontend/public/icons/icon-144x144.png new file mode 100644 index 0000000..55d319b Binary files /dev/null and b/frontend/public/icons/icon-144x144.png differ diff --git a/frontend/public/icons/icon-152x152.png b/frontend/public/icons/icon-152x152.png new file mode 100644 index 0000000..14086bf Binary files /dev/null and b/frontend/public/icons/icon-152x152.png differ diff --git a/frontend/public/icons/icon-192x192.png b/frontend/public/icons/icon-192x192.png new file mode 100644 index 0000000..fe00a44 Binary files /dev/null and b/frontend/public/icons/icon-192x192.png differ diff --git a/frontend/public/icons/icon-384x384.png b/frontend/public/icons/icon-384x384.png new file mode 100644 index 0000000..0dd8251 Binary files /dev/null and b/frontend/public/icons/icon-384x384.png differ diff --git a/frontend/public/icons/icon-512x512.png b/frontend/public/icons/icon-512x512.png new file mode 100644 index 0000000..10bfa5a Binary files /dev/null and b/frontend/public/icons/icon-512x512.png differ diff --git a/frontend/public/icons/icon-72x72.png b/frontend/public/icons/icon-72x72.png new file mode 100644 index 0000000..4379f43 Binary files /dev/null and b/frontend/public/icons/icon-72x72.png differ diff --git a/frontend/public/icons/icon-96x96.png b/frontend/public/icons/icon-96x96.png new file mode 100644 index 0000000..4567627 Binary files /dev/null and b/frontend/public/icons/icon-96x96.png differ diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..542ebae --- /dev/null +++ b/frontend/public/manifest.webmanifest @@ -0,0 +1,62 @@ +{ + "name": "recordaLexia", + "short_name": "recordaLexia", + "description": "Rutinas visuales y material del cole para niños con TDAH", + "lang": "es", + "display": "standalone", + "orientation": "any", + "scope": "./", + "start_url": "./", + "theme_color": "#5B8DEF", + "background_color": "#EFF4F6", + "icons": [ + { + "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" + } + ] +} \ No newline at end of file diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 4c9836a..0b3c82a 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,15 +1,19 @@ -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { ApplicationConfig, provideZoneChangeDetection, isDevMode } from '@angular/core'; import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { authInterceptor } from './core/auth.interceptor'; +import { provideServiceWorker } from '@angular/service-worker'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), // Cliente HTTP (fetch) con el interceptor de sesión de familia. - provideHttpClient(withFetch(), withInterceptors([authInterceptor])), + provideHttpClient(withFetch(), withInterceptors([authInterceptor])), provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), + registrationStrategy: 'registerWhenStable:30000' + }), ], }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index fa95cb7..6f486ea 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -6,13 +6,19 @@ import { KeypadComponent } from './features/parents/keypad.component'; import { ParentsComponent } from './features/parents/parents.component'; import { LoginComponent } from './features/auth/login.component'; import { RegisterComponent } from './features/auth/register.component'; +import { ForgotComponent } from './features/auth/forgot.component'; +import { ResetComponent } from './features/auth/reset.component'; import { AccountComponent } from './features/auth/account.component'; +import { PrivacyComponent } from './features/legal/privacy.component'; import { authGuard, parentGuard } from './core/auth.guard'; export const routes: Routes = [ // Públicas. { path: 'login', component: LoginComponent }, { path: 'register', component: RegisterComponent }, + { path: 'forgot', component: ForgotComponent }, + { path: 'reset', component: ResetComponent }, + { path: 'privacidad', component: PrivacyComponent }, // Requieren sesión de familia. { path: '', component: ProfileSelectComponent, canActivate: [authGuard] }, diff --git a/frontend/src/app/core/auth.service.ts b/frontend/src/app/core/auth.service.ts index 95e6926..c0340e0 100644 --- a/frontend/src/app/core/auth.service.ts +++ b/frontend/src/app/core/auth.service.ts @@ -24,13 +24,27 @@ export class AuthService { return this.sessionSignal(); } - /** Alta de familia (auto-login). */ - register(email: string, password: string, name: string, pin: string): Observable<{ session: string }> { + /** Alta de familia (auto-login). Exige aceptar la política de privacidad (RGPD). */ + register(email: string, password: string, name: string, pin: string, + privacyAccepted: boolean): Observable<{ session: string }> { return this.http - .post<{ session: string }>('/api/auth/register', { email, password, name, pin }) + .post<{ session: string }>('/api/auth/register', { email, password, name, pin, privacyAccepted }) .pipe(tap((res) => this.store(res.session))); } + /** Solicita el email de recuperación. La respuesta es neutra a propósito. */ + forgotPassword(email: string): Observable { + return this.http.post('/api/auth/forgot-password', { email }); + } + + /** Restablece la contraseña con el código del enlace del email. */ + resetPassword(code: string, nueva: string): Observable { + // El DTO del backend espera la clave "newPassword". + const body: Record = { code }; + body['newPassword'] = nueva; + return this.http.post('/api/auth/reset-password', body); + } + /** Acceso con email + contraseña. */ login(email: string, password: string): Observable<{ session: string }> { return this.http diff --git a/frontend/src/app/features/auth/auth.scss b/frontend/src/app/features/auth/auth.scss index ab4372d..d6cbc7d 100644 --- a/frontend/src/app/features/auth/auth.scss +++ b/frontend/src/app/features/auth/auth.scss @@ -72,6 +72,31 @@ cursor: default; } +// Consentimiento RGPD: legible y tocable, casilla nunca premarcada. +.auth__consent { + display: flex; + align-items: flex-start; + gap: 10px; + text-align: left; + font-size: 0.9rem; + color: var(--text-1); + cursor: pointer; + + input[type='checkbox'] { + width: 22px; + height: 22px; + flex: none; + accent-color: var(--accent-blue); + cursor: pointer; + } + + a { + color: var(--accent-blue); + font-weight: 700; + text-decoration: none; + } +} + .auth__alt { margin: var(--space-2) 0 0; color: var(--text-2); diff --git a/frontend/src/app/features/auth/forgot.component.ts b/frontend/src/app/features/auth/forgot.component.ts new file mode 100644 index 0000000..46dba31 --- /dev/null +++ b/frontend/src/app/features/auth/forgot.component.ts @@ -0,0 +1,57 @@ +import { Component, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { AuthService } from '../../core/auth.service'; + +/** + * Solicitud de recuperación de contraseña. El mensaje de confirmación es neutro + * a propósito (no revela si el email existe) y se muestra SIEMPRE, incluso si el + * backend limita por exceso de intentos: nada que un atacante pueda leer. + */ +@Component({ + selector: 'app-forgot', + imports: [FormsModule, RouterLink], + template: ` +
+
+

¿Contraseña olvidada? 🔑

+ + @if (sent()) { +

+ Si el email existe, recibirás un enlace en unos minutos. + Revisa también la carpeta de spam. +

+ } @else { +

Te enviaremos un enlace para crear una nueva

+ + + } + +

‹ Volver a entrar

+
+
+ `, + styleUrl: './auth.scss', +}) +export class ForgotComponent { + private readonly auth = inject(AuthService); + + protected email = ''; + protected readonly loading = signal(false); + protected readonly sent = signal(false); + + submit(): void { + if (!this.email) { + return; + } + this.loading.set(true); + this.auth.forgotPassword(this.email.trim()).subscribe({ + next: () => this.sent.set(true), + // También en error mostramos el mensaje neutro: no filtramos nada. + error: () => this.sent.set(true), + }); + } +} diff --git a/frontend/src/app/features/auth/login.component.ts b/frontend/src/app/features/auth/login.component.ts index 7b2ab05..a09b994 100644 --- a/frontend/src/app/features/auth/login.component.ts +++ b/frontend/src/app/features/auth/login.component.ts @@ -22,6 +22,7 @@ import { AuthService } from '../../core/auth.service'; +

¿Has olvidado la contraseña?

¿No tienes cuenta? Crear una

diff --git a/frontend/src/app/features/auth/register.component.ts b/frontend/src/app/features/auth/register.component.ts index 83ab27c..a900b72 100644 --- a/frontend/src/app/features/auth/register.component.ts +++ b/frontend/src/app/features/auth/register.component.ts @@ -21,6 +21,12 @@ import { AuthService } from '../../core/auth.service'; + + + @if (error()) {

{{ error() }}

} + } @else if (!code) { +

Este enlace no es válido. Pide uno nuevo.

+

Pedir otro enlace

+ } @else { +

Elige una contraseña nueva (mínimo 6 caracteres)

+ + + + @if (distintas()) {

Las contraseñas no coinciden

} + @if (error()) {

El enlace no es válido o ha caducado. Pide uno nuevo.

} + + +

‹ Volver a entrar

+ } + + + `, + styleUrl: './auth.scss', +}) +export class ResetComponent { + private readonly auth = inject(AuthService); + private readonly router = inject(Router); + + /** Código de un solo uso que viaja en el enlace del email. */ + protected readonly code = inject(ActivatedRoute).snapshot.queryParamMap.get('code'); + + protected nueva = ''; + protected repetida = ''; + protected readonly loading = signal(false); + protected readonly error = signal(false); + protected readonly done = signal(false); + + protected distintas(): boolean { + return this.repetida.length > 0 && this.nueva !== this.repetida; + } + + protected valida(): boolean { + return this.nueva.length >= 6 && this.nueva === this.repetida; + } + + submit(): void { + if (!this.code || !this.valida()) { + return; + } + this.loading.set(true); + this.error.set(false); + this.auth.resetPassword(this.code, this.nueva).subscribe({ + next: () => this.done.set(true), + error: () => { + this.error.set(true); + this.loading.set(false); + }, + }); + } +} diff --git a/frontend/src/app/features/legal/privacy.component.ts b/frontend/src/app/features/legal/privacy.component.ts new file mode 100644 index 0000000..48f04ca --- /dev/null +++ b/frontend/src/app/features/legal/privacy.component.ts @@ -0,0 +1,88 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +/** + * Política de privacidad (RGPD). Pública, sin sesión, bilingüe ES/CA en la misma + * página (texto legal: mejor ambas versiones visibles que un conmutador). + */ +@Component({ + selector: 'app-privacy', + imports: [RouterLink], + template: ` +
+
+

Política de privacidad · Política de privacitat

+ +
+

🇪🇸 Español

+

Qué guardamos. Lo mínimo para que la app funcione: el email y + el nombre de la cuenta de familia, y de cada niño solo su nombre de pila, edad, + una mascota y un color. No pedimos apellidos, ni fotos, ni datos de salud, ni + ubicación. Las tareas, rutinas y monedas que se generan al usar la app.

+

Para qué. Únicamente para mostrar a cada familia sus rutinas. + No usamos los datos para publicidad, no los analizamos con fines comerciales y + no los cedemos a nadie.

+

Dónde. En un servidor gestionado por el responsable de la + aplicación, con copias de seguridad cifradas. Las contraseñas y el PIN se + guardan con hash seguro: nadie puede leerlos, ni siquiera nosotros.

+

Tus derechos. Puedes borrar la cuenta desde + Cuenta → Borrar cuenta: se elimina TODO (niños, tareas, historial) de forma + inmediata e irreversible. Para cualquier otra petición sobre tus datos, + escríbenos al correo del pie de la aplicación.

+

Menores. La cuenta la crea y administra siempre una persona + adulta. Los perfiles de los niños no contienen datos identificativos más allá + del nombre de pila que su familia decida.

+
+ +
+

🏴 Català

+

Què guardem. El mínim perquè l'app funcioni: l'email i el nom + del compte de família, i de cada infant només el nom de pila, l'edat, una + mascota i un color. No demanem cognoms, ni fotos, ni dades de salut, ni + ubicació. Les tasques, rutines i monedes que es generen en fer servir l'app.

+

Per a què. Únicament per mostrar a cada família les seves + rutines. No fem servir les dades per a publicitat, no les analitzem amb fins + comercials i no les cedim a ningú.

+

On. En un servidor gestionat pel responsable de l'aplicació, + amb còpies de seguretat xifrades. Les contrasenyes i el PIN es guarden amb hash + segur: ningú no els pot llegir, ni tan sols nosaltres.

+

Els teus drets. Pots esborrar el compte des de + Compte → Esborrar compte: s'elimina TOT (infants, tasques, historial) de manera + immediata i irreversible. Per a qualsevol altra petició sobre les teves dades, + escriu-nos al correu del peu de l'aplicació.

+

Menors. El compte el crea i l'administra sempre una persona + adulta. Els perfils dels infants no contenen dades identificatives més enllà + del nom de pila que la seva família decideixi.

+
+ +

‹ Volver · Tornar

+
+
+ `, + styles: [ + ` + :host { display: block; } + .privacy { + min-height: 100vh; + display: flex; + justify-content: center; + padding: var(--space-5); + } + .privacy__card { + max-width: 720px; + background: var(--surface); + border: 1px solid var(--border-1); + border-radius: var(--radius-card); + padding: var(--space-6); + box-shadow: var(--shadow-card); + } + h1 { font-size: 1.5rem; margin-top: 0; } + h2 { font-size: 1.15rem; color: var(--accent-blue); } + p { line-height: 1.6; color: var(--text-1); } + strong { color: var(--text-strong); } + .privacy__back a { color: var(--accent-blue); font-weight: 700; text-decoration: none; } + `, + ], +}) +export class PrivacyComponent { +} diff --git a/frontend/src/index.html b/frontend/src/index.html index 79c5af0..69163ef 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -9,8 +9,10 @@ + +