i18n vocabulaire exos (fr/pt-BR) + Web Push rappels de série
Build & Deploy / build (push) Successful in 17s
Build & Deploy / build (push) Successful in 17s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@
|
|||||||
.env.*
|
.env.*
|
||||||
vendor
|
vendor
|
||||||
node_modules
|
node_modules
|
||||||
|
bootstrap/cache/packages.php
|
||||||
|
bootstrap/cache/services.php
|
||||||
storage/app/dataset
|
storage/app/dataset
|
||||||
storage/app/private/*
|
storage/app/private/*
|
||||||
storage/logs/*
|
storage/logs/*
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"functional/streaks": "*",
|
"functional/streaks": "*",
|
||||||
"functional/tracking": "*",
|
"functional/tracking": "*",
|
||||||
"functional/users": "*",
|
"functional/users": "*",
|
||||||
|
"laravel-notification-channels/webpush": "^11.0",
|
||||||
"laravel/framework": "^13.8",
|
"laravel/framework": "^13.8",
|
||||||
"laravel/octane": "^2.17",
|
"laravel/octane": "^2.17",
|
||||||
"laravel/sanctum": "^4.3",
|
"laravel/sanctum": "^4.3",
|
||||||
|
|||||||
Generated
+487
-7
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "d4b09f66cc8dc9ea7f3f370e110cd76c",
|
"content-hash": "1fd71e714394b125185f4b12ac7e5165",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "anourvalar/eloquent-serialize",
|
"name": "anourvalar/eloquent-serialize",
|
||||||
@@ -374,16 +374,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
"version": "0.18.0",
|
"version": "0.17.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/brick/math.git",
|
"url": "https://github.com/brick/math.git",
|
||||||
"reference": "82944324d1c1bdb2c2618e89978d4e2ad78d69ad"
|
"reference": "8189e751995f9e15729c1aa2f89fa8f166ffe818"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/brick/math/zipball/82944324d1c1bdb2c2618e89978d4e2ad78d69ad",
|
"url": "https://api.github.com/repos/brick/math/zipball/8189e751995f9e15729c1aa2f89fa8f166ffe818",
|
||||||
"reference": "82944324d1c1bdb2c2618e89978d4e2ad78d69ad",
|
"reference": "8189e751995f9e15729c1aa2f89fa8f166ffe818",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -421,7 +421,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/brick/math/issues",
|
"issues": "https://github.com/brick/math/issues",
|
||||||
"source": "https://github.com/brick/math/tree/0.18.0"
|
"source": "https://github.com/brick/math/tree/0.17.2"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -429,7 +429,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-06-14T18:21:03+00:00"
|
"time": "2026-05-25T20:34:43+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "carbonphp/carbon-doctrine-types",
|
"name": "carbonphp/carbon-doctrine-types",
|
||||||
@@ -2508,6 +2508,72 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-10-12T15:31:36+00:00"
|
"time": "2025-10-12T15:31:36+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "laravel-notification-channels/webpush",
|
||||||
|
"version": "11.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/laravel-notification-channels/webpush.git",
|
||||||
|
"reference": "85b577e64459a9df06a24062e2b300abbaa99fa9"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/laravel-notification-channels/webpush/zipball/85b577e64459a9df06a24062e2b300abbaa99fa9",
|
||||||
|
"reference": "85b577e64459a9df06a24062e2b300abbaa99fa9",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/notifications": "^12.0|^13.0",
|
||||||
|
"illuminate/support": "^12.0|^13.0",
|
||||||
|
"minishlink/web-push": "^10.0.1",
|
||||||
|
"php": "^8.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"larastan/larastan": "^3.1",
|
||||||
|
"laravel/pint": "^1.25",
|
||||||
|
"mockery/mockery": "^1.0",
|
||||||
|
"orchestra/testbench": "^9.2|^10.0|^11.0",
|
||||||
|
"phpunit/phpunit": "^11.5.3|^12.5.12|^13.1.11",
|
||||||
|
"rector/rector": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"NotificationChannels\\WebPush\\WebPushServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"NotificationChannels\\WebPush\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Cretu Eusebiu",
|
||||||
|
"email": "me@cretueusebiu.com",
|
||||||
|
"homepage": "http://cretueusebiu.com",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Joost de Bruijn",
|
||||||
|
"email": "joost@aqualabs.nl",
|
||||||
|
"role": "Maintainer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Web Push Notifications driver for Laravel.",
|
||||||
|
"homepage": "https://github.com/laravel-notification-channels/webpush",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/laravel-notification-channels/webpush/issues",
|
||||||
|
"source": "https://github.com/laravel-notification-channels/webpush/tree/11.0.0"
|
||||||
|
},
|
||||||
|
"time": "2026-05-24T13:22:27+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/framework",
|
"name": "laravel/framework",
|
||||||
"version": "v13.20.0",
|
"version": "v13.20.0",
|
||||||
@@ -4089,6 +4155,77 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-07-13T12:28:52+00:00"
|
"time": "2026-07-13T12:28:52+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "minishlink/web-push",
|
||||||
|
"version": "v10.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/web-push-libs/web-push-php.git",
|
||||||
|
"reference": "c922021b4ed1a61e6604d8dc33a2e0378b4382e3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/c922021b4ed1a61e6604d8dc33a2e0378b4382e3",
|
||||||
|
"reference": "c922021b4ed1a61e6604d8dc33a2e0378b4382e3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-openssl": "*",
|
||||||
|
"guzzlehttp/guzzle": "^7.9.2",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/log": "^2.0|^3.0",
|
||||||
|
"spomky-labs/base64url": "^2.0.4",
|
||||||
|
"symfony/polyfill-php83": "^1.33",
|
||||||
|
"web-token/jwt-library": "^3.4.9|^4.0.6"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^v3.92.2",
|
||||||
|
"phpstan/phpstan": "^2.1.33",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^2.0",
|
||||||
|
"phpstan/phpstan-strict-rules": "^2.0",
|
||||||
|
"phpunit/phpunit": "^11.5.46|^12.5.2",
|
||||||
|
"symfony/polyfill-iconv": "^1.33"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "Optional for performance.",
|
||||||
|
"ext-gmp": "Optional for performance."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Minishlink\\WebPush\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Louis Lagrange",
|
||||||
|
"email": "lagrange.louis@gmail.com",
|
||||||
|
"homepage": "https://github.com/Minishlink"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Web Push library for PHP",
|
||||||
|
"homepage": "https://github.com/web-push-libs/web-push-php",
|
||||||
|
"keywords": [
|
||||||
|
"Push API",
|
||||||
|
"WebPush",
|
||||||
|
"notifications",
|
||||||
|
"push",
|
||||||
|
"web"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/web-push-libs/web-push-php/issues",
|
||||||
|
"source": "https://github.com/web-push-libs/web-push-php/tree/v10.1.0"
|
||||||
|
},
|
||||||
|
"time": "2026-05-28T09:37:37+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "monolog/monolog",
|
"name": "monolog/monolog",
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
@@ -6214,6 +6351,180 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-04-27T14:27:52+00:00"
|
"time": "2026-04-27T14:27:52+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spomky-labs/base64url",
|
||||||
|
"version": "v2.0.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Spomky-Labs/base64url.git",
|
||||||
|
"reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d",
|
||||||
|
"reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/extension-installer": "^1.0",
|
||||||
|
"phpstan/phpstan": "^0.11|^0.12",
|
||||||
|
"phpstan/phpstan-beberlei-assert": "^0.11|^0.12",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^0.11|^0.12",
|
||||||
|
"phpstan/phpstan-phpunit": "^0.11|^0.12",
|
||||||
|
"phpstan/phpstan-strict-rules": "^0.11|^0.12"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Base64Url\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Florent Morselli",
|
||||||
|
"homepage": "https://github.com/Spomky-Labs/base64url/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Base 64 URL Safe Encoding/Decoding PHP Library",
|
||||||
|
"homepage": "https://github.com/Spomky-Labs/base64url",
|
||||||
|
"keywords": [
|
||||||
|
"base64",
|
||||||
|
"rfc4648",
|
||||||
|
"safe",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Spomky-Labs/base64url/issues",
|
||||||
|
"source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Spomky",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/FlorentMorselli",
|
||||||
|
"type": "patreon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2020-11-03T09:10:25+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spomky-labs/pki-framework",
|
||||||
|
"version": "1.5.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Spomky-Labs/pki-framework.git",
|
||||||
|
"reference": "e0d61661962560c1cedfef02b51b431e720aae78"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/e0d61661962560c1cedfef02b51b431e720aae78",
|
||||||
|
"reference": "e0d61661962560c1cedfef02b51b431e720aae78",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": ">=8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ekino/phpstan-banned-code": "^1.0|^2.0|^3.0",
|
||||||
|
"ext-gmp": "*",
|
||||||
|
"ext-openssl": "*",
|
||||||
|
"infection/infection": "^0.28|^0.29|^0.31",
|
||||||
|
"php-parallel-lint/php-parallel-lint": "^1.3",
|
||||||
|
"phpstan/extension-installer": "^1.3|^2.0",
|
||||||
|
"phpstan/phpstan": "^1.8|^2.0",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^1.0|^2.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.1|^2.0",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1.3|^2.0",
|
||||||
|
"phpunit/phpunit": "^10.1|^11.0|^12.0",
|
||||||
|
"rector/rector": "^1.0|^2.0",
|
||||||
|
"roave/security-advisories": "dev-latest",
|
||||||
|
"symfony/string": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/var-dumper": "^6.4|^7.0|^8.0",
|
||||||
|
"symplify/easy-coding-standard": "^12.0 || ^13.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "For better performance (or GMP)",
|
||||||
|
"ext-gmp": "For better performance (or BCMath)",
|
||||||
|
"ext-openssl": "For OpenSSL based cyphering"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"SpomkyLabs\\Pki\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Joni Eskelinen",
|
||||||
|
"email": "jonieske@gmail.com",
|
||||||
|
"role": "Original developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Florent Morselli",
|
||||||
|
"email": "florent.morselli@spomky-labs.com",
|
||||||
|
"role": "Spomky-Labs PKI Framework developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.",
|
||||||
|
"homepage": "https://github.com/spomky-labs/pki-framework",
|
||||||
|
"keywords": [
|
||||||
|
"DER",
|
||||||
|
"Private Key",
|
||||||
|
"ac",
|
||||||
|
"algorithm identifier",
|
||||||
|
"asn.1",
|
||||||
|
"asn1",
|
||||||
|
"attribute certificate",
|
||||||
|
"certificate",
|
||||||
|
"certification request",
|
||||||
|
"cryptography",
|
||||||
|
"csr",
|
||||||
|
"decrypt",
|
||||||
|
"ec",
|
||||||
|
"encrypt",
|
||||||
|
"pem",
|
||||||
|
"pkcs",
|
||||||
|
"public key",
|
||||||
|
"rsa",
|
||||||
|
"sign",
|
||||||
|
"signature",
|
||||||
|
"verify",
|
||||||
|
"x.509",
|
||||||
|
"x.690",
|
||||||
|
"x509",
|
||||||
|
"x690"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Spomky-Labs/pki-framework/issues",
|
||||||
|
"source": "https://github.com/Spomky-Labs/pki-framework/tree/1.5.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Spomky",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/FlorentMorselli",
|
||||||
|
"type": "patreon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-07-16T10:28:45+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v8.1.0",
|
"version": "v8.1.0",
|
||||||
@@ -7851,6 +8162,86 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-04-10T16:19:22+00:00"
|
"time": "2026-04-10T16:19:22+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/polyfill-php83",
|
||||||
|
"version": "v1.38.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||||
|
"reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8",
|
||||||
|
"reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"url": "https://github.com/symfony/polyfill",
|
||||||
|
"name": "symfony/polyfill"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"bootstrap.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Polyfill\\Php83\\": ""
|
||||||
|
},
|
||||||
|
"classmap": [
|
||||||
|
"Resources/stubs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"compatibility",
|
||||||
|
"polyfill",
|
||||||
|
"portable",
|
||||||
|
"shim"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-27T06:51:48+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php84",
|
"name": "symfony/polyfill-php84",
|
||||||
"version": "v1.38.1",
|
"version": "v1.38.1",
|
||||||
@@ -9264,6 +9655,95 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-04-26T05:33:54+00:00"
|
"time": "2026-04-26T05:33:54+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "web-token/jwt-library",
|
||||||
|
"version": "4.1.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/web-token/jwt-library.git",
|
||||||
|
"reference": "fbcbf2c276d04d8b056f5c2957815abd5dfb704d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/web-token/jwt-library/zipball/fbcbf2c276d04d8b056f5c2957815abd5dfb704d",
|
||||||
|
"reference": "fbcbf2c276d04d8b056f5c2957815abd5dfb704d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"brick/math": "^0.12|^0.13|^0.14|^0.15|^0.16|^0.17",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/clock": "^1.0",
|
||||||
|
"spomky-labs/pki-framework": "^1.2.1"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"spomky-labs/jose": "*"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance",
|
||||||
|
"ext-gmp": "GMP or BCMath is highly recommended to improve the library performance",
|
||||||
|
"ext-openssl": "For key management (creation, optimization, etc.) and some algorithms (AES, RSA, ECDSA, etc.)",
|
||||||
|
"ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys",
|
||||||
|
"paragonie/sodium_compat": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys",
|
||||||
|
"spomky-labs/aes-key-wrap": "For all Key Wrapping algorithms (AxxxKW, AxxxGCMKW, PBES2-HSxxx+AyyyKW...)",
|
||||||
|
"symfony/console": "Needed to use console commands",
|
||||||
|
"symfony/http-client": "To enable JKU/X5U support."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Jose\\Component\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Florent Morselli",
|
||||||
|
"homepage": "https://github.com/Spomky"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "All contributors",
|
||||||
|
"homepage": "https://github.com/web-token/jwt-framework/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "JWT library",
|
||||||
|
"homepage": "https://github.com/web-token",
|
||||||
|
"keywords": [
|
||||||
|
"JOSE",
|
||||||
|
"JWE",
|
||||||
|
"JWK",
|
||||||
|
"JWKSet",
|
||||||
|
"JWS",
|
||||||
|
"Jot",
|
||||||
|
"RFC7515",
|
||||||
|
"RFC7516",
|
||||||
|
"RFC7517",
|
||||||
|
"RFC7518",
|
||||||
|
"RFC7519",
|
||||||
|
"RFC7520",
|
||||||
|
"bundle",
|
||||||
|
"jwa",
|
||||||
|
"jwt",
|
||||||
|
"symfony"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/web-token/jwt-library/issues",
|
||||||
|
"source": "https://github.com/web-token/jwt-library/tree/4.1.7"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Spomky",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/FlorentMorselli",
|
||||||
|
"type": "patreon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-06-06T18:12:39+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "xefi/laravel-osdd",
|
"name": "xefi/laravel-osdd",
|
||||||
"version": "v2.0.0",
|
"version": "v2.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use NotificationChannels\WebPush\PushSubscription;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are the keys for authentication (VAPID).
|
||||||
|
* These keys must be safely stored and should not change.
|
||||||
|
*/
|
||||||
|
'vapid' => [
|
||||||
|
'subject' => env('VAPID_SUBJECT'),
|
||||||
|
'public_key' => env('VAPID_PUBLIC_KEY'),
|
||||||
|
'private_key' => env('VAPID_PRIVATE_KEY'),
|
||||||
|
'pem_file' => env('VAPID_PEM_FILE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is model that will be used to for push subscriptions.
|
||||||
|
*/
|
||||||
|
'model' => PushSubscription::class,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the name of the table that will be created by the migration and
|
||||||
|
* used by the PushSubscription model shipped with this package.
|
||||||
|
*/
|
||||||
|
'table_name' => env('WEBPUSH_DB_TABLE', 'push_subscriptions'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the database connection that will be used by the migration and
|
||||||
|
* the PushSubscription model shipped with this package.
|
||||||
|
*/
|
||||||
|
'database_connection' => env('WEBPUSH_DB_CONNECTION', env('DB_CONNECTION', 'mysql')),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Guzzle client options used by Minishlink\WebPush.
|
||||||
|
*/
|
||||||
|
'client_options' => [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The automatic padding in bytes used by Minishlink\WebPush.
|
||||||
|
* Set to false to support Firefox Android with v1 endpoint.
|
||||||
|
*/
|
||||||
|
'automatic_padding' => env('WEBPUSH_AUTOMATIC_PADDING', true),
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::connection(config('webpush.database_connection'))->create(config('webpush.table_name'), function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->morphs('subscribable', 'push_subscriptions_subscribable_morph_idx');
|
||||||
|
$table->string('endpoint', 500)->unique();
|
||||||
|
$table->string('public_key')->nullable();
|
||||||
|
$table->string('auth_token')->nullable();
|
||||||
|
$table->string('content_encoding')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::connection(config('webpush.database_connection'))->dropIfExists(config('webpush.table_name'));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traduction du vocabulaire contrôlé des exercices (zones, équipement, muscles, catégories).
|
||||||
|
* La DB reste en anglais (canonique) ; l'API traduit à la sortie selon la locale.
|
||||||
|
* Map à plat term(anglais, minuscule) => traduction. Repli = terme anglais si absent.
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
'fr' => [
|
||||||
|
// zones / catégories
|
||||||
|
'back' => 'dos', 'cardio' => 'cardio', 'chest' => 'pectoraux', 'lower arms' => 'avant-bras',
|
||||||
|
'lower legs' => 'mollets', 'neck' => 'cou', 'shoulders' => 'épaules', 'upper arms' => 'bras',
|
||||||
|
'upper legs' => 'cuisses', 'waist' => 'abdominaux',
|
||||||
|
// équipement
|
||||||
|
'assisted' => 'assisté', 'band' => 'élastique', 'barbell' => 'barre', 'body weight' => 'poids du corps',
|
||||||
|
'bosu ball' => 'bosu', 'cable' => 'poulie', 'dumbbell' => 'haltère', 'elliptical machine' => 'vélo elliptique',
|
||||||
|
'ez barbell' => 'barre EZ', 'hammer' => 'hammer', 'kettlebell' => 'kettlebell',
|
||||||
|
'leverage machine' => 'machine à levier', 'medicine ball' => 'medecine ball',
|
||||||
|
'olympic barbell' => 'barre olympique', 'resistance band' => 'bande de résistance', 'roller' => 'rouleau',
|
||||||
|
'rope' => 'corde', 'skierg machine' => 'skierg', 'sled machine' => 'traîneau', 'smith machine' => 'smith machine',
|
||||||
|
'stability ball' => 'swiss ball', 'stationary bike' => "vélo d'appartement", 'stepmill machine' => 'stepper',
|
||||||
|
'tire' => 'pneu', 'trap bar' => 'trap bar', 'upper body ergometer' => 'ergomètre bras',
|
||||||
|
'weighted' => 'lesté', 'wheel roller' => 'roue abdominale',
|
||||||
|
// muscles cibles
|
||||||
|
'abductors' => 'abducteurs', 'abs' => 'abdominaux', 'adductors' => 'adducteurs', 'biceps' => 'biceps',
|
||||||
|
'calves' => 'mollets', 'cardiovascular system' => 'système cardiovasculaire', 'delts' => 'deltoïdes',
|
||||||
|
'forearms' => 'avant-bras', 'glutes' => 'fessiers', 'hamstrings' => 'ischio-jambiers', 'lats' => 'grand dorsal',
|
||||||
|
'levator scapulae' => 'élévateur de la scapula', 'pectorals' => 'pectoraux', 'quads' => 'quadriceps',
|
||||||
|
'serratus anterior' => 'dentelé antérieur', 'spine' => 'colonne vertébrale', 'traps' => 'trapèzes',
|
||||||
|
'triceps' => 'triceps', 'upper back' => 'haut du dos',
|
||||||
|
// muscles secondaires (compléments)
|
||||||
|
'abdominals' => 'abdominaux', 'ankle stabilizers' => 'stabilisateurs de cheville', 'ankles' => 'chevilles',
|
||||||
|
'brachialis' => 'brachial', 'chest ' => 'pectoraux', 'core' => 'gainage', 'deltoids' => 'deltoïdes',
|
||||||
|
'feet' => 'pieds', 'grip muscles' => 'muscles de préhension', 'groin' => 'aine', 'hands' => 'mains',
|
||||||
|
'hip flexors' => 'fléchisseurs de hanche', 'inner thighs' => 'intérieur des cuisses',
|
||||||
|
'latissimus dorsi' => 'grand dorsal', 'lower abs' => 'bas des abdominaux', 'lower back' => 'bas du dos',
|
||||||
|
'obliques' => 'obliques', 'quadriceps' => 'quadriceps', 'rear deltoids' => 'deltoïdes postérieurs',
|
||||||
|
'rhomboids' => 'rhomboïdes', 'rotator cuff' => 'coiffe des rotateurs', 'shins' => 'tibias',
|
||||||
|
'soleus' => 'soléaire', 'sternocleidomastoid' => 'sterno-cléido-mastoïdien', 'trapezius' => 'trapèze',
|
||||||
|
'upper chest' => 'haut des pectoraux', 'wrist extensors' => 'extenseurs du poignet',
|
||||||
|
'wrist flexors' => 'fléchisseurs du poignet', 'wrists' => 'poignets',
|
||||||
|
],
|
||||||
|
|
||||||
|
'pt-BR' => [
|
||||||
|
// zonas / categorias
|
||||||
|
'back' => 'costas', 'cardio' => 'cardio', 'chest' => 'peito', 'lower arms' => 'antebraços',
|
||||||
|
'lower legs' => 'panturrilhas', 'neck' => 'pescoço', 'shoulders' => 'ombros', 'upper arms' => 'braços',
|
||||||
|
'upper legs' => 'coxas', 'waist' => 'abdômen',
|
||||||
|
// equipamento
|
||||||
|
'assisted' => 'assistido', 'band' => 'faixa elástica', 'barbell' => 'barra', 'body weight' => 'peso corporal',
|
||||||
|
'bosu ball' => 'bola bosu', 'cable' => 'cabo', 'dumbbell' => 'halter', 'elliptical machine' => 'elíptico',
|
||||||
|
'ez barbell' => 'barra EZ', 'hammer' => 'hammer', 'kettlebell' => 'kettlebell',
|
||||||
|
'leverage machine' => 'máquina de alavanca', 'medicine ball' => 'bola medicinal',
|
||||||
|
'olympic barbell' => 'barra olímpica', 'resistance band' => 'faixa de resistência', 'roller' => 'rolo',
|
||||||
|
'rope' => 'corda', 'skierg machine' => 'skierg', 'sled machine' => 'trenó', 'smith machine' => 'máquina smith',
|
||||||
|
'stability ball' => 'bola de estabilidade', 'stationary bike' => 'bicicleta ergométrica',
|
||||||
|
'stepmill machine' => 'escada ergométrica', 'tire' => 'pneu', 'trap bar' => 'barra hexagonal',
|
||||||
|
'upper body ergometer' => 'ergômetro de braços', 'weighted' => 'com peso', 'wheel roller' => 'roda abdominal',
|
||||||
|
// músculos alvo
|
||||||
|
'abductors' => 'abdutores', 'abs' => 'abdômen', 'adductors' => 'adutores', 'biceps' => 'bíceps',
|
||||||
|
'calves' => 'panturrilhas', 'cardiovascular system' => 'sistema cardiovascular', 'delts' => 'deltoides',
|
||||||
|
'forearms' => 'antebraços', 'glutes' => 'glúteos', 'hamstrings' => 'posteriores da coxa', 'lats' => 'dorsais',
|
||||||
|
'levator scapulae' => 'levantador da escápula', 'pectorals' => 'peitorais', 'quads' => 'quadríceps',
|
||||||
|
'serratus anterior' => 'serrátil anterior', 'spine' => 'coluna', 'traps' => 'trapézios',
|
||||||
|
'triceps' => 'tríceps', 'upper back' => 'parte superior das costas',
|
||||||
|
// músculos secundários (complementos)
|
||||||
|
'abdominals' => 'abdominais', 'ankle stabilizers' => 'estabilizadores do tornozelo', 'ankles' => 'tornozelos',
|
||||||
|
'brachialis' => 'braquial', 'core' => 'core', 'deltoids' => 'deltoides', 'feet' => 'pés',
|
||||||
|
'grip muscles' => 'músculos de preensão', 'groin' => 'virilha', 'hands' => 'mãos',
|
||||||
|
'hip flexors' => 'flexores do quadril', 'inner thighs' => 'parte interna das coxas',
|
||||||
|
'latissimus dorsi' => 'grande dorsal', 'lower abs' => 'abdômen inferior', 'lower back' => 'lombar',
|
||||||
|
'obliques' => 'oblíquos', 'quadriceps' => 'quadríceps', 'rear deltoids' => 'deltoides posteriores',
|
||||||
|
'rhomboids' => 'romboides', 'rotator cuff' => 'manguito rotador', 'shins' => 'canelas',
|
||||||
|
'soleus' => 'sóleo', 'sternocleidomastoid' => 'esternocleidomastóideo', 'trapezius' => 'trapézio',
|
||||||
|
'upper chest' => 'parte superior do peito', 'wrist extensors' => 'extensores do punho',
|
||||||
|
'wrist flexors' => 'flexores do punho', 'wrists' => 'punhos',
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -30,6 +30,8 @@ class ExercisesController
|
|||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->paginate((int) $request->query('per_page', self::DEFAULT_PER_PAGE));
|
->paginate((int) $request->query('per_page', self::DEFAULT_PER_PAGE));
|
||||||
|
|
||||||
|
ExerciseListResource::$locale = $this->resolveLocale($request);
|
||||||
|
|
||||||
return ExerciseListResource::collection($exercises);
|
return ExerciseListResource::collection($exercises);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,14 +41,24 @@ class ExercisesController
|
|||||||
return new ExerciseResource($exercise, $this->resolveLocale($request));
|
return new ExerciseResource($exercise, $this->resolveLocale($request));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /api/exercises/facets — valeurs distinctes pour alimenter les filtres du front. */
|
/**
|
||||||
public function facets(): JsonResponse
|
* GET /api/exercises/facets — valeurs distinctes pour les filtres.
|
||||||
|
* Chaque valeur = { value: anglais (canonique, envoyé au filtre), label: traduit }.
|
||||||
|
*/
|
||||||
|
public function facets(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$locale = $this->resolveLocale($request);
|
||||||
|
|
||||||
|
$facet = fn (string $column) => Exercise::query()
|
||||||
|
->whereNotNull($column)->distinct()->orderBy($column)->pluck($column)
|
||||||
|
->map(fn ($v) => ['value' => $v, 'label' => Exercise::localizeTerm($v, $locale)])
|
||||||
|
->values();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'body_parts' => Exercise::query()->whereNotNull('body_part')->distinct()->orderBy('body_part')->pluck('body_part')->values(),
|
'body_parts' => $facet('body_part'),
|
||||||
'equipments' => Exercise::query()->whereNotNull('equipment')->distinct()->orderBy('equipment')->pluck('equipment')->values(),
|
'equipments' => $facet('equipment'),
|
||||||
'targets' => Exercise::query()->whereNotNull('target')->distinct()->orderBy('target')->pluck('target')->values(),
|
'targets' => $facet('target'),
|
||||||
'categories' => Exercise::query()->whereNotNull('category')->distinct()->orderBy('category')->pluck('category')->values(),
|
'categories' => $facet('category'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,23 +2,29 @@
|
|||||||
|
|
||||||
namespace Functional\Exercises\Http\Resources;
|
namespace Functional\Exercises\Http\Resources;
|
||||||
|
|
||||||
|
use Functional\Exercises\Models\Exercise;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forme allégée d'un exercice pour les listes (GET /api/exercises).
|
* Forme allégée d'un exercice pour les listes (GET /api/exercises), libellés localisés.
|
||||||
|
* La locale est posée par le contrôleur avant `collection()` (collection() n'accepte pas d'argument par item).
|
||||||
*/
|
*/
|
||||||
class ExerciseListResource extends JsonResource
|
class ExerciseListResource extends JsonResource
|
||||||
{
|
{
|
||||||
|
public static string $locale = 'fr';
|
||||||
|
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
|
$l = self::$locale;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'slug' => $this->slug,
|
'slug' => $this->slug,
|
||||||
'body_part' => $this->body_part,
|
'body_part' => Exercise::localizeTerm($this->body_part, $l),
|
||||||
'equipment' => $this->equipment,
|
'equipment' => Exercise::localizeTerm($this->equipment, $l),
|
||||||
'target' => $this->target,
|
'target' => Exercise::localizeTerm($this->target, $l),
|
||||||
'image_url' => $this->image_url,
|
'image_url' => $this->image_url,
|
||||||
'gif_url' => $this->gif_url,
|
'gif_url' => $this->gif_url,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -17,16 +17,20 @@ class ExerciseResource extends JsonResource
|
|||||||
|
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
|
$l = $this->locale;
|
||||||
|
$model = $this->resource;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'slug' => $this->slug,
|
'slug' => $this->slug,
|
||||||
'category' => $this->category,
|
'body_part' => $model::localizeTerm($this->body_part, $l),
|
||||||
'equipment' => $this->equipment,
|
'category' => $model::localizeTerm($this->category, $l),
|
||||||
'target' => $this->target,
|
'equipment' => $model::localizeTerm($this->equipment, $l),
|
||||||
'muscle_group' => $this->muscle_group,
|
'target' => $model::localizeTerm($this->target, $l),
|
||||||
'secondary_muscles' => $this->secondary_muscles ?? [],
|
'muscle_group' => $model::localizeTerm($this->muscle_group, $l),
|
||||||
'instructions' => $this->resource->instructionsFor($this->locale),
|
'secondary_muscles' => $model::localizeTerms($this->secondary_muscles, $l),
|
||||||
|
'instructions' => $model->instructionsFor($l),
|
||||||
'image_url' => $this->image_url,
|
'image_url' => $this->image_url,
|
||||||
'gif_url' => $this->gif_url,
|
'gif_url' => $this->gif_url,
|
||||||
'attribution' => $this->attribution,
|
'attribution' => $this->attribution,
|
||||||
|
|||||||
@@ -82,6 +82,24 @@ class Exercise extends Model
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Traduit un terme du vocabulaire contrôlé (zone/équipement/muscle) selon la locale. Repli = anglais. */
|
||||||
|
public static function localizeTerm(?string $value, string $locale): ?string
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = config('exercise_i18n.' . $locale, []);
|
||||||
|
|
||||||
|
return $map[mb_strtolower($value)] ?? $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int,string>|null $values @return array<int,string> */
|
||||||
|
public static function localizeTerms(?array $values, string $locale): array
|
||||||
|
{
|
||||||
|
return array_values(array_map(fn ($v) => self::localizeTerm($v, $locale), $values ?? []));
|
||||||
|
}
|
||||||
|
|
||||||
/** Recherche plein-texte simple (nom / muscle cible / équipement). */
|
/** Recherche plein-texte simple (nom / muscle cible / équipement). */
|
||||||
public function scopeSearch(Builder $query, ?string $term): Builder
|
public function scopeSearch(Builder $query, ?string $term): Builder
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,5 +28,6 @@ class ExercisesServiceProvider extends LayerServiceProvider
|
|||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
$this->mergeConfigFrom(__DIR__ . '/../../config/exercises.php', 'exercises');
|
$this->mergeConfigFrom(__DIR__ . '/../../config/exercises.php', 'exercises');
|
||||||
|
$this->mergeConfigFrom(__DIR__ . '/../../config/exercise_i18n.php', 'exercise_i18n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('streaks', function (Blueprint $table) {
|
||||||
|
// Date du dernier rappel push envoyé (anti-doublon : 1 rappel/jour max).
|
||||||
|
$table->date('last_reminded_on')->nullable()->after('freeze_last_granted_on');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('streaks', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('last_reminded_on');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
// Define artisan commands here, e.g.:
|
// Rappels de série : chaque heure, la commande cible les users en fenêtre du soir (18h-22h locale), 1x/jour max.
|
||||||
// Artisan::command('layer:hello', fn() => info('Hello from this layer.'));
|
Schedule::command('streaks:remind')->hourly();
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Functional\Streaks\Console\Commands;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Functional\Streaks\Models\Streak;
|
||||||
|
use Functional\Streaks\Notifications\StreakAtRiskNotification;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rappelle aux utilisateurs de ne pas perdre leur série.
|
||||||
|
* Planifiée toutes les heures : pour chaque user dont la série est VIVANTE mais pas entretenue
|
||||||
|
* aujourd'hui, on envoie un push le soir (fenêtre 18h-22h locale), 1 fois/jour max.
|
||||||
|
*/
|
||||||
|
class RemindStreaksCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'streaks:remind {--force : ignore la fenêtre horaire (test)}';
|
||||||
|
|
||||||
|
protected $description = 'Envoie un rappel push aux utilisateurs dont la série est en danger.';
|
||||||
|
|
||||||
|
private const WINDOW_START = 18;
|
||||||
|
|
||||||
|
private const WINDOW_END = 22;
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$defaultTz = config('app.timezone', 'UTC');
|
||||||
|
$force = (bool) $this->option('force');
|
||||||
|
$sent = 0;
|
||||||
|
|
||||||
|
Streak::query()
|
||||||
|
->where('current_count', '>=', 1)
|
||||||
|
->with('user')
|
||||||
|
->chunkById(200, function ($streaks) use ($defaultTz, $force, &$sent) {
|
||||||
|
foreach ($streaks as $streak) {
|
||||||
|
$user = $streak->user;
|
||||||
|
if (! $user) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tz = $user->timezone ?: $defaultTz;
|
||||||
|
$now = Carbon::now($tz);
|
||||||
|
$today = $now->toDateString();
|
||||||
|
$yesterday = $now->copy()->subDay()->toDateString();
|
||||||
|
$lastActive = $streak->last_active_date?->toDateString();
|
||||||
|
$lastReminded = $streak->last_reminded_on?->toDateString();
|
||||||
|
|
||||||
|
// Fenêtre horaire du soir (sauf --force).
|
||||||
|
if (! $force && ($now->hour < self::WINDOW_START || $now->hour >= self::WINDOW_END)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Déjà entraîné aujourd'hui → rien à rappeler.
|
||||||
|
if ($lastActive === $today) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Série vivante = dernière activité HIER (sinon déjà cassée / gérée par les freezes).
|
||||||
|
if ($lastActive !== $yesterday) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Déjà rappelé aujourd'hui.
|
||||||
|
if ($lastReminded === $today) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Pas d'abonnement push → inutile.
|
||||||
|
if ($user->pushSubscriptions()->count() === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->notify(new StreakAtRiskNotification((int) $streak->current_count));
|
||||||
|
$streak->forceFill(['last_reminded_on' => $today])->save();
|
||||||
|
$sent++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->info("Rappels envoyés : {$sent}");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ class Streak extends Model
|
|||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id', 'current_count', 'longest_count', 'last_active_date',
|
'user_id', 'current_count', 'longest_count', 'last_active_date',
|
||||||
'freezes_available', 'freeze_last_granted_on',
|
'freezes_available', 'freeze_last_granted_on', 'last_reminded_on',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@@ -35,6 +35,7 @@ class Streak extends Model
|
|||||||
'last_active_date' => 'date',
|
'last_active_date' => 'date',
|
||||||
'freezes_available' => 'integer',
|
'freezes_available' => 'integer',
|
||||||
'freeze_last_granted_on' => 'date',
|
'freeze_last_granted_on' => 'date',
|
||||||
|
'last_reminded_on' => 'date',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Functional\Streaks\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use NotificationChannels\WebPush\WebPushChannel;
|
||||||
|
use NotificationChannels\WebPush\WebPushMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rappel « ta série est en danger » — push envoyé le soir si l'utilisateur n'a pas encore
|
||||||
|
* entraîné aujourd'hui alors que sa série est vivante. Textes localisés selon la locale du user.
|
||||||
|
*/
|
||||||
|
class StreakAtRiskNotification extends Notification
|
||||||
|
{
|
||||||
|
public function __construct(private readonly int $currentCount)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int,string> */
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [WebPushChannel::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toWebPush(object $notifiable): WebPushMessage
|
||||||
|
{
|
||||||
|
$locale = $notifiable->locale ?? 'fr';
|
||||||
|
$n = $this->currentCount;
|
||||||
|
|
||||||
|
[$title, $body] = match ($locale) {
|
||||||
|
'en' => ["🔥 Your {$n}-day streak!", 'Train today so you don\'t lose it.'],
|
||||||
|
'pt-BR' => ["🔥 Sua sequência de {$n} dias!", 'Treine hoje para não perdê-la.'],
|
||||||
|
default => ["🔥 Ta série de {$n} jours !", 'Entraîne-toi aujourd\'hui pour ne pas la perdre.'],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (new WebPushMessage())
|
||||||
|
->title($title)
|
||||||
|
->body($body)
|
||||||
|
->icon('/pwa-192.png')
|
||||||
|
->badge('/pwa-192.png')
|
||||||
|
->data(['url' => '/'])
|
||||||
|
->options(['TTL' => 12 * 3600, 'urgency' => 'high']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Functional\Streaks\Providers;
|
namespace Functional\Streaks\Providers;
|
||||||
|
|
||||||
|
use Functional\Streaks\Console\Commands\RemindStreaksCommand;
|
||||||
use Functional\Streaks\Database\Seeders\StreaksSeeder;
|
use Functional\Streaks\Database\Seeders\StreaksSeeder;
|
||||||
use Functional\Streaks\Listeners\UpdateStreakOnWorkoutCompleted;
|
use Functional\Streaks\Listeners\UpdateStreakOnWorkoutCompleted;
|
||||||
use Functional\Streaks\Services\StreakService;
|
use Functional\Streaks\Services\StreakService;
|
||||||
@@ -16,6 +17,7 @@ class StreaksServiceProvider extends LayerServiceProvider
|
|||||||
if ($this->app->runningInConsole()) {
|
if ($this->app->runningInConsole()) {
|
||||||
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
|
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
|
||||||
$this->loadSeeders([StreaksSeeder::class]);
|
$this->loadSeeders([StreaksSeeder::class]);
|
||||||
|
$this->commands([RemindStreaksCommand::class]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->withRouting(
|
$this->withRouting(
|
||||||
@@ -24,7 +26,6 @@ class StreaksServiceProvider extends LayerServiceProvider
|
|||||||
commands: __DIR__ . '/../../routes/console.php',
|
commands: __DIR__ . '/../../routes/console.php',
|
||||||
channels: __DIR__ . '/../../routes/channels.php',
|
channels: __DIR__ . '/../../routes/channels.php',
|
||||||
);
|
);
|
||||||
$this->loadSeeders([\Functional\Streaks\Database\Seeders\StreaksSeeder::class]);
|
|
||||||
|
|
||||||
// Le streak se met à jour à chaque séance terminée (événement porté par la couche tracking).
|
// Le streak se met à jour à chaque séance terminée (événement porté par la couche tracking).
|
||||||
Event::listen(WorkoutCompleted::class, UpdateStreakOnWorkoutCompleted::class);
|
Event::listen(WorkoutCompleted::class, UpdateStreakOnWorkoutCompleted::class);
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Functional\Users\Http\Controllers\AuthController;
|
use Functional\Users\Http\Controllers\AuthController;
|
||||||
|
use Functional\Users\Http\Controllers\PushController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
// Le préfixe api/ est ajouté par UsersServiceProvider::withRouting() : GET /api/me.
|
// Le préfixe api/ est ajouté par UsersServiceProvider::withRouting().
|
||||||
Route::get('me', [AuthController::class, 'me'])->middleware('auth:sanctum');
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
Route::get('me', [AuthController::class, 'me']);
|
||||||
|
|
||||||
|
// Web Push (rappels de série pilotés par le back)
|
||||||
|
Route::post('push/subscribe', [PushController::class, 'subscribe']);
|
||||||
|
Route::post('push/unsubscribe', [PushController::class, 'unsubscribe']);
|
||||||
|
Route::post('push/test', [PushController::class, 'test']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ class AuthController
|
|||||||
'longest' => $streak->longest_count,
|
'longest' => $streak->longest_count,
|
||||||
'freezes' => $streak->freezes_available,
|
'freezes' => $streak->freezes_available,
|
||||||
],
|
],
|
||||||
|
'push' => [
|
||||||
|
'vapid_public_key' => config('webpush.vapid.public_key'),
|
||||||
|
'enabled' => $user->pushSubscriptions()->exists(),
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Functional\Users\Http\Controllers;
|
||||||
|
|
||||||
|
use Functional\Users\Notifications\PushTestNotification;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abonnement Web Push (PWA). Le front envoie sa subscription ; on la stocke sur l'utilisateur.
|
||||||
|
* L'envoi réel des rappels est piloté par le back (commande planifiée) — cf couche streaks.
|
||||||
|
*/
|
||||||
|
class PushController
|
||||||
|
{
|
||||||
|
/** POST /api/push/subscribe — enregistre/actualise l'abonnement push du navigateur. */
|
||||||
|
public function subscribe(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'endpoint' => ['required', 'string'],
|
||||||
|
'keys.p256dh' => ['required', 'string'],
|
||||||
|
'keys.auth' => ['required', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->updatePushSubscription(
|
||||||
|
$data['endpoint'],
|
||||||
|
$data['keys']['p256dh'],
|
||||||
|
$data['keys']['auth'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json(['status' => 'subscribed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/push/unsubscribe — supprime l'abonnement (désactivation des rappels). */
|
||||||
|
public function unsubscribe(Request $request): Response
|
||||||
|
{
|
||||||
|
$request->validate(['endpoint' => ['required', 'string']]);
|
||||||
|
$request->user()->deletePushSubscription($request->input('endpoint'));
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/push/test — envoie une notif de test (bouton "tester" côté profil). */
|
||||||
|
public function test(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$request->user()->notify(new PushTestNotification());
|
||||||
|
|
||||||
|
return response()->json(['status' => 'sent']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
use NotificationChannels\WebPush\HasPushSubscriptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $locale
|
* @property string $locale
|
||||||
@@ -27,7 +28,7 @@ use Laravel\Sanctum\HasApiTokens;
|
|||||||
#[UseFactory(UserFactory::class)]
|
#[UseFactory(UserFactory::class)]
|
||||||
class User extends Authenticatable implements FilamentUser
|
class User extends Authenticatable implements FilamentUser
|
||||||
{
|
{
|
||||||
use HasApiTokens, HasFactory, Notifiable;
|
use HasApiTokens, HasFactory, HasPushSubscriptions, Notifiable;
|
||||||
|
|
||||||
/** Accès au back-office Filament (à affiner : rôle admin plus tard). */
|
/** Accès au back-office Filament (à affiner : rôle admin plus tard). */
|
||||||
public function canAccessPanel(Panel $panel): bool
|
public function canAccessPanel(Panel $panel): bool
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Functional\Users\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use NotificationChannels\WebPush\WebPushChannel;
|
||||||
|
use NotificationChannels\WebPush\WebPushMessage;
|
||||||
|
|
||||||
|
/** Notification de test envoyée quand l'utilisateur active/teste les rappels push. */
|
||||||
|
class PushTestNotification extends Notification
|
||||||
|
{
|
||||||
|
/** @return array<int,string> */
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [WebPushChannel::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toWebPush(object $notifiable): WebPushMessage
|
||||||
|
{
|
||||||
|
return (new WebPushMessage())
|
||||||
|
->title('🔥 StreakFit')
|
||||||
|
->body('Notifications activées — on te préviendra pour garder ta flamme !')
|
||||||
|
->icon('/pwa-192.png')
|
||||||
|
->badge('/pwa-192.png')
|
||||||
|
->data(['url' => '/'])
|
||||||
|
->options(['TTL' => 3600]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user