1 Commits

Author SHA1 Message Date
5fbd555bb3 Start of changing tabled to flexboxes 2026-01-07 08:18:56 +01:00
24 changed files with 414 additions and 622 deletions

View File

@@ -1,4 +1,4 @@
FROM php:8.4-cli FROM php:8.3-cli
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV PHP_MEMORY_LIMIT=1500M ENV PHP_MEMORY_LIMIT=1500M

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
echo "Updating project" echo "Updating project"
git pull origin master git pull origin master
bin/cacheclean bin/cli rm -rf var/cache
bin/composer install --no-dev
echo "Project updated" echo "Project updated"

467
composer.lock generated
View File

@@ -1796,21 +1796,22 @@
}, },
{ {
"name": "symfony/clock", "name": "symfony/clock",
"version": "v8.0.0", "version": "v7.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/clock.git", "url": "https://github.com/symfony/clock.git",
"reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" "reference": "9169f24776edde469914c1e7a1442a50f7a4e110"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110",
"reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", "reference": "9169f24776edde469914c1e7a1442a50f7a4e110",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4", "php": ">=8.2",
"psr/clock": "^1.0" "psr/clock": "^1.0",
"symfony/polyfill-php83": "^1.28"
}, },
"provide": { "provide": {
"psr/clock-implementation": "1.0" "psr/clock-implementation": "1.0"
@@ -1849,7 +1850,7 @@
"time" "time"
], ],
"support": { "support": {
"source": "https://github.com/symfony/clock/tree/v8.0.0" "source": "https://github.com/symfony/clock/tree/v7.4.0"
}, },
"funding": [ "funding": [
{ {
@@ -1869,37 +1870,38 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-11-12T15:46:48+00:00" "time": "2025-11-12T15:39:26+00:00"
}, },
{ {
"name": "symfony/config", "name": "symfony/config",
"version": "v8.0.3", "version": "v7.4.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/config.git", "url": "https://github.com/symfony/config.git",
"reference": "58063686fd7b8e676f14b5a4808cb85265c5216e" "reference": "800ce889e358a53a9678b3212b0c8cecd8c6aace"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/58063686fd7b8e676f14b5a4808cb85265c5216e", "url": "https://api.github.com/repos/symfony/config/zipball/800ce889e358a53a9678b3212b0c8cecd8c6aace",
"reference": "58063686fd7b8e676f14b5a4808cb85265c5216e", "reference": "800ce889e358a53a9678b3212b0c8cecd8c6aace",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4", "php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3", "symfony/deprecation-contracts": "^2.5|^3",
"symfony/filesystem": "^7.4|^8.0", "symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "^1.8" "symfony/polyfill-ctype": "~1.8"
}, },
"conflict": { "conflict": {
"symfony/finder": "<6.4",
"symfony/service-contracts": "<2.5" "symfony/service-contracts": "<2.5"
}, },
"require-dev": { "require-dev": {
"symfony/event-dispatcher": "^7.4|^8.0", "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/finder": "^7.4|^8.0", "symfony/finder": "^6.4|^7.0|^8.0",
"symfony/messenger": "^7.4|^8.0", "symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^7.4|^8.0" "symfony/yaml": "^6.4|^7.0|^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -1927,7 +1929,7 @@
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/config/tree/v8.0.3" "source": "https://github.com/symfony/config/tree/v7.4.3"
}, },
"funding": [ "funding": [
{ {
@@ -1947,7 +1949,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-23T14:52:06+00:00" "time": "2025-12-23T14:24:27+00:00"
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
@@ -2049,36 +2051,39 @@
}, },
{ {
"name": "symfony/dependency-injection", "name": "symfony/dependency-injection",
"version": "v8.0.3", "version": "v7.4.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/dependency-injection.git", "url": "https://github.com/symfony/dependency-injection.git",
"reference": "8db0d4c1dd4c533a29210c68074999ba45ad6d3e" "reference": "54122901b6d772e94f1e71a75e0533bc16563499"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8db0d4c1dd4c533a29210c68074999ba45ad6d3e", "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/54122901b6d772e94f1e71a75e0533bc16563499",
"reference": "8db0d4c1dd4c533a29210c68074999ba45ad6d3e", "reference": "54122901b6d772e94f1e71a75e0533bc16563499",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4", "php": ">=8.2",
"psr/container": "^1.1|^2.0", "psr/container": "^1.1|^2.0",
"symfony/deprecation-contracts": "^2.5|^3", "symfony/deprecation-contracts": "^2.5|^3",
"symfony/service-contracts": "^3.6", "symfony/service-contracts": "^3.6",
"symfony/var-exporter": "^7.4|^8.0" "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0"
}, },
"conflict": { "conflict": {
"ext-psr": "<1.1|>=2" "ext-psr": "<1.1|>=2",
"symfony/config": "<6.4",
"symfony/finder": "<6.4",
"symfony/yaml": "<6.4"
}, },
"provide": { "provide": {
"psr/container-implementation": "1.1|2.0", "psr/container-implementation": "1.1|2.0",
"symfony/service-implementation": "1.1|2.0|3.0" "symfony/service-implementation": "1.1|2.0|3.0"
}, },
"require-dev": { "require-dev": {
"symfony/config": "^7.4|^8.0", "symfony/config": "^6.4|^7.0|^8.0",
"symfony/expression-language": "^7.4|^8.0", "symfony/expression-language": "^6.4|^7.0|^8.0",
"symfony/yaml": "^7.4|^8.0" "symfony/yaml": "^6.4|^7.0|^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -2106,7 +2111,7 @@
"description": "Allows you to standardize and centralize the way objects are constructed in your application", "description": "Allows you to standardize and centralize the way objects are constructed in your application",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/dependency-injection/tree/v8.0.3" "source": "https://github.com/symfony/dependency-injection/tree/v7.4.3"
}, },
"funding": [ "funding": [
{ {
@@ -2126,7 +2131,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-23T14:52:06+00:00" "time": "2025-12-28T10:55:46+00:00"
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
@@ -2275,32 +2280,33 @@
}, },
{ {
"name": "symfony/error-handler", "name": "symfony/error-handler",
"version": "v8.0.0", "version": "v7.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/error-handler.git", "url": "https://github.com/symfony/error-handler.git",
"reference": "d77ec7dda0c274178745d152e82baf7ea827fd73" "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/d77ec7dda0c274178745d152e82baf7ea827fd73", "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2",
"reference": "d77ec7dda0c274178745d152e82baf7ea827fd73", "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4", "php": ">=8.2",
"psr/log": "^1|^2|^3", "psr/log": "^1|^2|^3",
"symfony/polyfill-php85": "^1.32", "symfony/polyfill-php85": "^1.32",
"symfony/var-dumper": "^7.4|^8.0" "symfony/var-dumper": "^6.4|^7.0|^8.0"
}, },
"conflict": { "conflict": {
"symfony/deprecation-contracts": "<2.5" "symfony/deprecation-contracts": "<2.5",
"symfony/http-kernel": "<6.4"
}, },
"require-dev": { "require-dev": {
"symfony/console": "^7.4|^8.0", "symfony/console": "^6.4|^7.0|^8.0",
"symfony/deprecation-contracts": "^2.5|^3", "symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-kernel": "^7.4|^8.0", "symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/serializer": "^7.4|^8.0", "symfony/serializer": "^6.4|^7.0|^8.0",
"symfony/webpack-encore-bundle": "^1.0|^2.0" "symfony/webpack-encore-bundle": "^1.0|^2.0"
}, },
"bin": [ "bin": [
@@ -2332,7 +2338,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code", "description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/error-handler/tree/v8.0.0" "source": "https://github.com/symfony/error-handler/tree/v7.4.0"
}, },
"funding": [ "funding": [
{ {
@@ -2352,28 +2358,28 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-11-05T14:36:47+00:00" "time": "2025-11-05T14:29:59+00:00"
}, },
{ {
"name": "symfony/event-dispatcher", "name": "symfony/event-dispatcher",
"version": "v8.0.0", "version": "v7.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/event-dispatcher.git", "url": "https://github.com/symfony/event-dispatcher.git",
"reference": "573f95783a2ec6e38752979db139f09fec033f03" "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d",
"reference": "573f95783a2ec6e38752979db139f09fec033f03", "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4", "php": ">=8.2",
"symfony/event-dispatcher-contracts": "^2.5|^3" "symfony/event-dispatcher-contracts": "^2.5|^3"
}, },
"conflict": { "conflict": {
"symfony/security-http": "<7.4", "symfony/dependency-injection": "<6.4",
"symfony/service-contracts": "<2.5" "symfony/service-contracts": "<2.5"
}, },
"provide": { "provide": {
@@ -2382,14 +2388,14 @@
}, },
"require-dev": { "require-dev": {
"psr/log": "^1|^2|^3", "psr/log": "^1|^2|^3",
"symfony/config": "^7.4|^8.0", "symfony/config": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^7.4|^8.0", "symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/error-handler": "^7.4|^8.0", "symfony/error-handler": "^6.4|^7.0|^8.0",
"symfony/expression-language": "^7.4|^8.0", "symfony/expression-language": "^6.4|^7.0|^8.0",
"symfony/framework-bundle": "^7.4|^8.0", "symfony/framework-bundle": "^6.4|^7.0|^8.0",
"symfony/http-foundation": "^7.4|^8.0", "symfony/http-foundation": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3",
"symfony/stopwatch": "^7.4|^8.0" "symfony/stopwatch": "^6.4|^7.0|^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -2417,7 +2423,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0"
}, },
"funding": [ "funding": [
{ {
@@ -2437,7 +2443,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-10-30T14:17:19+00:00" "time": "2025-10-28T09:38:46+00:00"
}, },
{ {
"name": "symfony/event-dispatcher-contracts", "name": "symfony/event-dispatcher-contracts",
@@ -2517,25 +2523,25 @@
}, },
{ {
"name": "symfony/filesystem", "name": "symfony/filesystem",
"version": "v8.0.1", "version": "v7.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/filesystem.git", "url": "https://github.com/symfony/filesystem.git",
"reference": "d937d400b980523dc9ee946bb69972b5e619058d" "reference": "d551b38811096d0be9c4691d406991b47c0c630a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d", "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a",
"reference": "d937d400b980523dc9ee946bb69972b5e619058d", "reference": "d551b38811096d0be9c4691d406991b47c0c630a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4", "php": ">=8.2",
"symfony/polyfill-ctype": "~1.8", "symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8" "symfony/polyfill-mbstring": "~1.8"
}, },
"require-dev": { "require-dev": {
"symfony/process": "^7.4|^8.0" "symfony/process": "^6.4|^7.0|^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -2563,7 +2569,7 @@
"description": "Provides basic utilities for the filesystem", "description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/filesystem/tree/v8.0.1" "source": "https://github.com/symfony/filesystem/tree/v7.4.0"
}, },
"funding": [ "funding": [
{ {
@@ -2583,27 +2589,27 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-01T09:13:36+00:00" "time": "2025-11-27T13:27:24+00:00"
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v8.0.3", "version": "v7.4.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
"reference": "dd3a2953570a283a2ba4e17063bb98c734cf5b12" "reference": "fffe05569336549b20a1be64250b40516d6e8d06"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/dd3a2953570a283a2ba4e17063bb98c734cf5b12", "url": "https://api.github.com/repos/symfony/finder/zipball/fffe05569336549b20a1be64250b40516d6e8d06",
"reference": "dd3a2953570a283a2ba4e17063bb98c734cf5b12", "reference": "fffe05569336549b20a1be64250b40516d6e8d06",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4" "php": ">=8.2"
}, },
"require-dev": { "require-dev": {
"symfony/filesystem": "^7.4|^8.0" "symfony/filesystem": "^6.4|^7.0|^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -2631,7 +2637,7 @@
"description": "Finds files and directories via an intuitive fluent interface", "description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/finder/tree/v8.0.3" "source": "https://github.com/symfony/finder/tree/v7.4.3"
}, },
"funding": [ "funding": [
{ {
@@ -2651,7 +2657,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-23T14:52:06+00:00" "time": "2025-12-23T14:50:43+00:00"
}, },
{ {
"name": "symfony/framework-bundle", "name": "symfony/framework-bundle",
@@ -2813,35 +2819,37 @@
}, },
{ {
"name": "symfony/http-foundation", "name": "symfony/http-foundation",
"version": "v8.0.3", "version": "v7.4.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-foundation.git", "url": "https://github.com/symfony/http-foundation.git",
"reference": "514ec3aa7982f296b0ad0825f75b6be5779ae9e7" "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/514ec3aa7982f296b0ad0825f75b6be5779ae9e7", "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a70c745d4cea48dbd609f4075e5f5cbce453bd52",
"reference": "514ec3aa7982f296b0ad0825f75b6be5779ae9e7", "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4", "php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "^1.1" "symfony/polyfill-mbstring": "^1.1"
}, },
"conflict": { "conflict": {
"doctrine/dbal": "<4.3" "doctrine/dbal": "<3.6",
"symfony/cache": "<6.4.12|>=7.0,<7.1.5"
}, },
"require-dev": { "require-dev": {
"doctrine/dbal": "^4.3", "doctrine/dbal": "^3.6|^4",
"predis/predis": "^1.1|^2.0", "predis/predis": "^1.1|^2.0",
"symfony/cache": "^7.4|^8.0", "symfony/cache": "^6.4.12|^7.1.5|^8.0",
"symfony/clock": "^7.4|^8.0", "symfony/clock": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^7.4|^8.0", "symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/expression-language": "^7.4|^8.0", "symfony/expression-language": "^6.4|^7.0|^8.0",
"symfony/http-kernel": "^7.4|^8.0", "symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/mime": "^7.4|^8.0", "symfony/mime": "^6.4|^7.0|^8.0",
"symfony/rate-limiter": "^7.4|^8.0" "symfony/rate-limiter": "^6.4|^7.0|^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -2869,7 +2877,7 @@
"description": "Defines an object-oriented layer for the HTTP specification", "description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/http-foundation/tree/v8.0.3" "source": "https://github.com/symfony/http-foundation/tree/v7.4.3"
}, },
"funding": [ "funding": [
{ {
@@ -2889,7 +2897,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-23T14:52:06+00:00" "time": "2025-12-23T14:23:49+00:00"
}, },
{ {
"name": "symfony/http-kernel", "name": "symfony/http-kernel",
@@ -3012,27 +3020,28 @@
}, },
{ {
"name": "symfony/intl", "name": "symfony/intl",
"version": "v8.0.1", "version": "v7.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/intl.git", "url": "https://github.com/symfony/intl.git",
"reference": "f9eca217ae8f2be0b3ad80723d6a3b518b90cd66" "reference": "2fa074de6c7faa6b54f2891fc22708f42245ed5c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/intl/zipball/f9eca217ae8f2be0b3ad80723d6a3b518b90cd66", "url": "https://api.github.com/repos/symfony/intl/zipball/2fa074de6c7faa6b54f2891fc22708f42245ed5c",
"reference": "f9eca217ae8f2be0b3ad80723d6a3b518b90cd66", "reference": "2fa074de6c7faa6b54f2891fc22708f42245ed5c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4" "php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3"
}, },
"conflict": { "conflict": {
"symfony/string": "<7.4" "symfony/string": "<7.1"
}, },
"require-dev": { "require-dev": {
"symfony/filesystem": "^7.4|^8.0", "symfony/filesystem": "^6.4|^7.0|^8.0",
"symfony/var-exporter": "^7.4|^8.0" "symfony/var-exporter": "^6.4|^7.0|^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -3077,7 +3086,7 @@
"localization" "localization"
], ],
"support": { "support": {
"source": "https://github.com/symfony/intl/tree/v8.0.1" "source": "https://github.com/symfony/intl/tree/v7.4.0"
}, },
"funding": [ "funding": [
{ {
@@ -3097,7 +3106,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-01T09:13:36+00:00" "time": "2025-11-27T13:27:24+00:00"
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
@@ -3434,6 +3443,86 @@
], ],
"time": "2024-12-23T08:48:59+00:00" "time": "2024-12-23T08:48:59+00:00"
}, },
{
"name": "symfony/polyfill-php83",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"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.33.0"
},
"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": "2025-07-08T02:45:35+00:00"
},
{ {
"name": "symfony/polyfill-php85", "name": "symfony/polyfill-php85",
"version": "v1.33.0", "version": "v1.33.0",
@@ -3688,34 +3777,35 @@
}, },
{ {
"name": "symfony/string", "name": "symfony/string",
"version": "v8.0.1", "version": "v7.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/string.git", "url": "https://github.com/symfony/string.git",
"reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003",
"reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4", "php": ">=8.2",
"symfony/polyfill-ctype": "^1.8", "symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/polyfill-intl-grapheme": "^1.33", "symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-normalizer": "^1.0", "symfony/polyfill-intl-grapheme": "~1.33",
"symfony/polyfill-mbstring": "^1.0" "symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0"
}, },
"conflict": { "conflict": {
"symfony/translation-contracts": "<2.5" "symfony/translation-contracts": "<2.5"
}, },
"require-dev": { "require-dev": {
"symfony/emoji": "^7.4|^8.0", "symfony/emoji": "^7.1|^8.0",
"symfony/http-client": "^7.4|^8.0", "symfony/http-client": "^6.4|^7.0|^8.0",
"symfony/intl": "^7.4|^8.0", "symfony/intl": "^6.4|^7.0|^8.0",
"symfony/translation-contracts": "^2.5|^3.0", "symfony/translation-contracts": "^2.5|^3.0",
"symfony/var-exporter": "^7.4|^8.0" "symfony/var-exporter": "^6.4|^7.0|^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -3754,7 +3844,7 @@
"utf8" "utf8"
], ],
"support": { "support": {
"source": "https://github.com/symfony/string/tree/v8.0.1" "source": "https://github.com/symfony/string/tree/v7.4.0"
}, },
"funding": [ "funding": [
{ {
@@ -3774,31 +3864,38 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-01T09:13:36+00:00" "time": "2025-11-27T13:27:24+00:00"
}, },
{ {
"name": "symfony/translation", "name": "symfony/translation",
"version": "v8.0.3", "version": "v7.4.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/translation.git", "url": "https://github.com/symfony/translation.git",
"reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d" "reference": "7ef27c65d78886f7599fdd5c93d12c9243ecf44d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/60a8f11f0e15c48f2cc47c4da53873bb5b62135d", "url": "https://api.github.com/repos/symfony/translation/zipball/7ef27c65d78886f7599fdd5c93d12c9243ecf44d",
"reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d", "reference": "7ef27c65d78886f7599fdd5c93d12c9243ecf44d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4", "php": ">=8.2",
"symfony/polyfill-mbstring": "^1.0", "symfony/deprecation-contracts": "^2.5|^3",
"symfony/translation-contracts": "^3.6.1" "symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^2.5.3|^3.3"
}, },
"conflict": { "conflict": {
"nikic/php-parser": "<5.0", "nikic/php-parser": "<5.0",
"symfony/config": "<6.4",
"symfony/console": "<6.4",
"symfony/dependency-injection": "<6.4",
"symfony/http-client-contracts": "<2.5", "symfony/http-client-contracts": "<2.5",
"symfony/service-contracts": "<2.5" "symfony/http-kernel": "<6.4",
"symfony/service-contracts": "<2.5",
"symfony/twig-bundle": "<6.4",
"symfony/yaml": "<6.4"
}, },
"provide": { "provide": {
"symfony/translation-implementation": "2.3|3.0" "symfony/translation-implementation": "2.3|3.0"
@@ -3806,17 +3903,17 @@
"require-dev": { "require-dev": {
"nikic/php-parser": "^5.0", "nikic/php-parser": "^5.0",
"psr/log": "^1|^2|^3", "psr/log": "^1|^2|^3",
"symfony/config": "^7.4|^8.0", "symfony/config": "^6.4|^7.0|^8.0",
"symfony/console": "^7.4|^8.0", "symfony/console": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^7.4|^8.0", "symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/finder": "^7.4|^8.0", "symfony/finder": "^6.4|^7.0|^8.0",
"symfony/http-client-contracts": "^2.5|^3.0", "symfony/http-client-contracts": "^2.5|^3.0",
"symfony/http-kernel": "^7.4|^8.0", "symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/intl": "^7.4|^8.0", "symfony/intl": "^6.4|^7.0|^8.0",
"symfony/polyfill-intl-icu": "^1.21", "symfony/polyfill-intl-icu": "^1.21",
"symfony/routing": "^7.4|^8.0", "symfony/routing": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^7.4|^8.0" "symfony/yaml": "^6.4|^7.0|^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -3847,7 +3944,7 @@
"description": "Provides tools to internationalize your application", "description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/translation/tree/v8.0.3" "source": "https://github.com/symfony/translation/tree/v7.4.3"
}, },
"funding": [ "funding": [
{ {
@@ -3867,7 +3964,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-21T10:59:45+00:00" "time": "2025-12-29T09:31:36+00:00"
}, },
{ {
"name": "symfony/translation-contracts", "name": "symfony/translation-contracts",
@@ -3953,58 +4050,67 @@
}, },
{ {
"name": "symfony/twig-bridge", "name": "symfony/twig-bridge",
"version": "v8.0.3", "version": "v7.4.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/twig-bridge.git", "url": "https://github.com/symfony/twig-bridge.git",
"reference": "2a2978a44127bae9aaee0ed5319954eb492d81c3" "reference": "43c922fce020060c65b0fd54bfd8def3b38949b6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/twig-bridge/zipball/2a2978a44127bae9aaee0ed5319954eb492d81c3", "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/43c922fce020060c65b0fd54bfd8def3b38949b6",
"reference": "2a2978a44127bae9aaee0ed5319954eb492d81c3", "reference": "43c922fce020060c65b0fd54bfd8def3b38949b6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4", "php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/translation-contracts": "^2.5|^3", "symfony/translation-contracts": "^2.5|^3",
"twig/twig": "^3.21" "twig/twig": "^3.21"
}, },
"conflict": { "conflict": {
"phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0" "phpdocumentor/type-resolver": "<1.4.0",
"symfony/console": "<6.4",
"symfony/form": "<6.4",
"symfony/http-foundation": "<6.4",
"symfony/http-kernel": "<6.4",
"symfony/mime": "<6.4",
"symfony/serializer": "<6.4",
"symfony/translation": "<6.4",
"symfony/workflow": "<6.4"
}, },
"require-dev": { "require-dev": {
"egulias/email-validator": "^2.1.10|^3|^4", "egulias/email-validator": "^2.1.10|^3|^4",
"league/html-to-markdown": "^5.0", "league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/asset": "^7.4|^8.0", "symfony/asset": "^6.4|^7.0|^8.0",
"symfony/asset-mapper": "^7.4|^8.0", "symfony/asset-mapper": "^6.4|^7.0|^8.0",
"symfony/console": "^7.4|^8.0", "symfony/console": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^7.4|^8.0", "symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/emoji": "^7.4|^8.0", "symfony/emoji": "^7.1|^8.0",
"symfony/expression-language": "^7.4|^8.0", "symfony/expression-language": "^6.4|^7.0|^8.0",
"symfony/finder": "^7.4|^8.0", "symfony/finder": "^6.4|^7.0|^8.0",
"symfony/form": "^7.4.1|^8.0.1", "symfony/form": "^6.4.30|~7.3.8|^7.4.1|^8.0.1",
"symfony/html-sanitizer": "^7.4|^8.0", "symfony/html-sanitizer": "^6.4|^7.0|^8.0",
"symfony/http-foundation": "^7.4|^8.0", "symfony/http-foundation": "^7.3|^8.0",
"symfony/http-kernel": "^7.4|^8.0", "symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/intl": "^7.4|^8.0", "symfony/intl": "^6.4|^7.0|^8.0",
"symfony/mime": "^7.4|^8.0", "symfony/mime": "^6.4|^7.0|^8.0",
"symfony/polyfill-intl-icu": "^1.0", "symfony/polyfill-intl-icu": "~1.0",
"symfony/property-info": "^7.4|^8.0", "symfony/property-info": "^6.4|^7.0|^8.0",
"symfony/routing": "^7.4|^8.0", "symfony/routing": "^6.4|^7.0|^8.0",
"symfony/security-acl": "^2.8|^3.0", "symfony/security-acl": "^2.8|^3.0",
"symfony/security-core": "^7.4|^8.0", "symfony/security-core": "^6.4|^7.0|^8.0",
"symfony/security-csrf": "^7.4|^8.0", "symfony/security-csrf": "^6.4|^7.0|^8.0",
"symfony/security-http": "^7.4|^8.0", "symfony/security-http": "^6.4|^7.0|^8.0",
"symfony/serializer": "^7.4|^8.0", "symfony/serializer": "^6.4.3|^7.0.3|^8.0",
"symfony/stopwatch": "^7.4|^8.0", "symfony/stopwatch": "^6.4|^7.0|^8.0",
"symfony/translation": "^7.4|^8.0", "symfony/translation": "^6.4|^7.0|^8.0",
"symfony/validator": "^7.4|^8.0", "symfony/validator": "^6.4|^7.0|^8.0",
"symfony/web-link": "^7.4|^8.0", "symfony/web-link": "^6.4|^7.0|^8.0",
"symfony/workflow": "^7.4|^8.0", "symfony/workflow": "^6.4|^7.0|^8.0",
"symfony/yaml": "^7.4|^8.0", "symfony/yaml": "^6.4|^7.0|^8.0",
"twig/cssinliner-extra": "^3", "twig/cssinliner-extra": "^3",
"twig/inky-extra": "^3", "twig/inky-extra": "^3",
"twig/markdown-extra": "^3" "twig/markdown-extra": "^3"
@@ -4035,7 +4141,7 @@
"description": "Provides integration for Twig with various Symfony components", "description": "Provides integration for Twig with various Symfony components",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/twig-bridge/tree/v8.0.3" "source": "https://github.com/symfony/twig-bridge/tree/v7.4.3"
}, },
"funding": [ "funding": [
{ {
@@ -4055,7 +4161,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-16T08:10:18+00:00" "time": "2025-12-16T08:02:06+00:00"
}, },
{ {
"name": "symfony/twig-bundle", "name": "symfony/twig-bundle",
@@ -4236,25 +4342,26 @@
}, },
{ {
"name": "symfony/var-exporter", "name": "symfony/var-exporter",
"version": "v8.0.0", "version": "v7.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/var-exporter.git", "url": "https://github.com/symfony/var-exporter.git",
"reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" "reference": "03a60f169c79a28513a78c967316fbc8bf17816f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f",
"reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", "reference": "03a60f169c79a28513a78c967316fbc8bf17816f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.4" "php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3"
}, },
"require-dev": { "require-dev": {
"symfony/property-access": "^7.4|^8.0", "symfony/property-access": "^6.4|^7.0|^8.0",
"symfony/serializer": "^7.4|^8.0", "symfony/serializer": "^6.4|^7.0|^8.0",
"symfony/var-dumper": "^7.4|^8.0" "symfony/var-dumper": "^6.4|^7.0|^8.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -4292,7 +4399,7 @@
"serialize" "serialize"
], ],
"support": { "support": {
"source": "https://github.com/symfony/var-exporter/tree/v8.0.0" "source": "https://github.com/symfony/var-exporter/tree/v7.4.0"
}, },
"funding": [ "funding": [
{ {
@@ -4312,7 +4419,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-11-05T18:53:00+00:00" "time": "2025-09-11T10:15:23+00:00"
}, },
{ {
"name": "twig/cache-extra", "name": "twig/cache-extra",
@@ -4681,5 +4788,5 @@
"ext-json": "*" "ext-json": "*"
}, },
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.9.0" "plugin-api-version": "2.6.0"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

After

Width:  |  Height:  |  Size: 199 KiB

View File

@@ -6,13 +6,11 @@ namespace Krzysiej\RyobiCrawler\Command;
use Illuminate\Database\Capsule\Manager as Capsule; use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Krzysiej\RyobiCrawler\Models\Country;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use function Symfony\Component\Clock\now;
#[AsCommand(name: 'app:migrate', description: 'Create database and rum migrations')] #[AsCommand(name: 'app:migrate', description: 'Create database and rum migrations')]
class Migrate extends Command class Migrate extends Command
@@ -26,10 +24,11 @@ class Migrate extends Command
public function execute(InputInterface $input, OutputInterface $output): int public function execute(InputInterface $input, OutputInterface $output): int
{ {
if ($input->getOption(self::RECREATE_OPTION)) { if (true === $input->hasOption(self::RECREATE_OPTION)) {
unlink(__DIR__ . '/../../database.sqlite'); unlink(__DIR__ . '/../../database.sqlite');
touch(__DIR__ . '/../../database.sqlite'); //sleep(5);
} }
touch(__DIR__ . '/../../database.sqlite');
$capsule = new Capsule; $capsule = new Capsule;
$capsule->addConnection([ $capsule->addConnection([
'driver' => 'sqlite', 'driver' => 'sqlite',
@@ -40,9 +39,6 @@ class Migrate extends Command
$this->createProductsTable(); $this->createProductsTable();
$this->createPricesTable(); $this->createPricesTable();
$this->createStocksTable(); $this->createStocksTable();
$this->addColumns();
$this->createCountriesTable();
$this->index();
return Command::SUCCESS; return Command::SUCCESS;
} }
@@ -81,51 +77,6 @@ class Migrate extends Command
} }
} }
public function createCountriesTable(): void
{
if (!Capsule::schema()->hasTable('countries')) {
Capsule::schema()->create('countries', function (Blueprint $table) {
$table->increments('id');
$table->text('countryName');
$table->text('productsUrl');
$table->text('cultureCode');
$table->text('currency');
$table->text('locale');
$table->timestamps();
});
}
if (Capsule::schema()->hasTable('countries') && !Country::where('countryName', 'Poland')->exists()) {
$id = Capsule::table('countries')->insertGetId(
[
'countryName' => 'Poland',
'productsUrl' => 'https://pl.ryobitools.eu/api/product-listing/get-products',
'cultureCode' => 'pl-PL',
'currency' => 'PLN',
'locale' => 'pl',
'created_at' => now(),
'updated_at' => now(),
]);
}
if (Capsule::schema()->hasTable('countries') && !Country::where('countryName', 'UK')->exists()) {
Capsule::table('countries')->insert([
'countryName' => 'UK',
'productsUrl' => 'https://uk.ryobitools.eu/api/product-listing/get-products',
'cultureCode' => 'en-GB',
'currency' => 'GBP',
'locale' => 'en',
'created_at' => now(),
'updated_at' => now(),
]
);
}
if (!Capsule::schema()->hasColumn('products', 'country_id')) {
Capsule::schema()->table('products', function (Blueprint $table) use ($id) {
$table->foreignId('country_id')->default($id)->references('id')->on('countries');
});
}
}
public function createStocksTable(): void public function createStocksTable(): void
{ {
if (!Capsule::schema()->hasTable('stocks')) { if (!Capsule::schema()->hasTable('stocks')) {
@@ -143,57 +94,4 @@ class Migrate extends Command
}); });
} }
} }
public function addColumns(): void
{
if (!Capsule::schema()->hasColumn('products', 'priceCurrent')) {
Capsule::schema()->table('products', function (Blueprint $table) {
$table->float('priceCurrent')->default(0);
});
}
if (!Capsule::schema()->hasColumn('products', 'priceLowest')) {
Capsule::schema()->table('products', function (Blueprint $table) {
$table->float('priceLowest')->default(0);
});
}
if (!Capsule::schema()->hasColumn('products', 'productStandardPrice')) {
Capsule::schema()->table('products', function (Blueprint $table) {
$table->float('productStandardPrice')->default(0);
});
}
if (!Capsule::schema()->hasColumn('products', 'lowestProductPrice30Days')) {
Capsule::schema()->table('products', function (Blueprint $table) {
$table->float('lowestProductPrice30Days')->default(0);
});
}
if (!Capsule::schema()->hasColumn('products', 'lastSeen')) {
Capsule::schema()->table('products', function (Blueprint $table) {
$table->date('lastSeen')->nullable();
});
}
if (!Capsule::schema()->hasColumn('products', 'stock')) {
Capsule::schema()->table('products', function (Blueprint $table) {
$table->integer('stock')->nullable();
});
}
}
public function index(): void
{
if (!count(Capsule::select('SELECT name FROM sqlite_master WHERE type = "index" and name = "products_skuid_country_id_unique"'))) {
Capsule::schema()->table('products', function (Blueprint $table) {
$table->integer('skuID')->unique(false)->change();
$table->unique(['skuID', 'country_id']);
});
}
Capsule::schema()->table('products', function (Blueprint $table) {
$table->foreign('id')->references('product_id')->on('stocks');
});
}
} }

View File

@@ -7,7 +7,6 @@ namespace Krzysiej\RyobiCrawler\Command;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Database\Capsule\Manager as Capsule; use Illuminate\Database\Capsule\Manager as Capsule;
use Krzysiej\RyobiCrawler\Models\Country;
use Krzysiej\RyobiCrawler\Models\Price; use Krzysiej\RyobiCrawler\Models\Price;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Krzysiej\RyobiCrawler\Models\Stock; use Krzysiej\RyobiCrawler\Models\Stock;
@@ -34,61 +33,33 @@ class ScrapeWebsite extends Command
public function execute(InputInterface $input, OutputInterface $output): int public function execute(InputInterface $input, OutputInterface $output): int
{ {
$output->writeln('Scrape products');
$progress = new ProgressBar($output); $progress = new ProgressBar($output);
$countries = Country::all();
foreach($countries as $country) {
$output->writeln('Country name: ' . $country->countryName."\n");
$progress->start();
$products = $this->getProducts($country);
$progress->setMaxSteps(count($products));
foreach ($products as $product) {
$this->saveProduct($product, $country);
$progress->advance();
}
$progress->finish();
$output->writeln('');
$output->writeln('Scrape products - DONE');
$output->writeln('');
}
$output->writeln('Update prices');
$products = Product::all();
$progress->setMaxSteps(count($products));
$progress->start(); $progress->start();
foreach($products as $product) { $products = $this->getProducts();
$newestPrice = $product->newestPrice; $progress->setMaxSteps(count($products));
$currentStock = $product->currentStock; foreach ($products as $product) {
$product->priceCurrent = $newestPrice->price; $this->saveProduct($product);
$product->productStandardPrice = $newestPrice->productStandardPrice;
$product->lowestProductPrice30Days = $newestPrice->lowestProductPrice30Days;
$product->priceLowest = $product->lowestPrice->price;
$product->lastSeen = $newestPrice->created_at->format('Y-m-d');
$product->stock = $currentStock->stock;
$product->save(['timestamps' => false]);
$progress->advance(); $progress->advance();
} }
$progress->finish(); $progress->finish();
$output->writeln(''); $output->writeln('');
$output->writeln('Update prices - DONE'); $output->writeln('DONE');
$output->writeln('COMMAND - DONE');
return Command::SUCCESS; return Command::SUCCESS;
} }
private function getProducts(Country $country): array private function getProducts(): array
{ {
$products = []; $products = [];
$page = 0; $page = 0;
do { do {
try { try {
$res = $this->client->request('POST', $country->productsUrl, [ $res = $this->client->request('POST', 'https://pl.ryobitools.eu/api/product-listing/get-products', [
'form_params' => [ 'form_params' => [
"includePreviousPages" => false, "includePreviousPages" => false,
"pageIndex" => $page, "pageIndex" => $page,
"pageSize" => 100, "pageSize" => 100,
"cultureCode" => $country->cultureCode, "cultureCode" => "pl-PL",
] ]
]); ]);
$responseObject = json_decode($res->getBody()->getContents()); $responseObject = json_decode($res->getBody()->getContents());
@@ -103,23 +74,21 @@ class ScrapeWebsite extends Command
return $products; return $products;
} }
private function saveProduct(\stdClass $product, Country $country): void private function saveProduct(\stdClass $product): void
{ {
/** @var Product $productModel */ /** @var Product $productModel */
$productModel = Product::firstOrNew(['skuID' => $product->skuID, 'country_id' => $country->id]); $productModel = Product::firstOrNew(['skuID' => $product->skuID]);
$productModel->skuID = $product->skuID; $productModel->skuID = $product->skuID;
$productModel->name = $product->name; $productModel->name = $product->name;
$productModel->availableQuantity = $product->availableQuantity; $productModel->availableQuantity = $product->availableQuantity;
$productModel->categories = str_replace(['/', ' '], '', $product->categories); $productModel->categories = $product->categories;
$productModel->image = $product->image; $productModel->image = $product->image;
$productModel->subTitle = $product->subTitle; $productModel->subTitle = $product->subTitle;
$productModel->variantCode = $product->variantCode; $productModel->variantCode = $product->variantCode;
$productModel->modelCode = $product->modelCode; $productModel->modelCode = $product->modelCode;
$productModel->url = $product->url; $productModel->url = $product->url;
$productModel->lastSeen = date("Y-m-d");
$productModel->touch('updated_at'); $productModel->touch('updated_at');
$productModel->country()->associate($country);
$productModel->save(); $productModel->save();
$priceExists = $productModel->price()->whereRaw("strftime('%Y-%m-%d', created_at) = ?", [date('Y-m-d')])->exists(); $priceExists = $productModel->price()->whereRaw("strftime('%Y-%m-%d', created_at) = ?", [date('Y-m-d')])->exists();

View File

@@ -8,43 +8,22 @@ use Symfony\Component\Routing\Attribute\Route;
final class CategoryController extends BaseController final class CategoryController extends BaseController
{ {
#[Route('/category/{category?}', name: 'app_category')] #[Route('/category/{category}', name: 'app_category')]
public function __invoke(?string $category): Response public function __invoke(string $category): Response
{ {
if($this->cache->getItem('list_category_'.$category)->isHit()) { if($this->cache->getItem('list_category_'.$category)->isHit()) {
return $this->render('productList.html.twig', ['listType' => 'category_'.$category]); return $this->render('productList.html.twig', ['listType' => 'category_'.$category]);
} }
/** @var Product[] $products */
$products = Product::with(['price', 'lowestPrice']) $products = Product::with(['price', 'lowestPrice'])
->selectRaw('products.*') ->selectRaw('products.*')
->distinct('products.id') ->distinct('products.id')
->when(!is_null($category) , fn ($q) => $q->whereRaw('json_each.value = ?', [$category]))
->fromRaw('products, json_each(products.categories)') ->fromRaw('products, json_each(products.categories)')
->whereRaw('json_each.value = ?', [$category])
->orderByDesc('starred') ->orderByDesc('starred')
->orderByDesc('created_by') ->orderByDesc('created_by')
->get(); ->get();
$categoriesTree = []; return $this->render('productList.html.twig', ['products' => $products, 'listType' => 'category_'.$category]);
foreach ($products as $product) {
$categoriesTree = $this->addToTree($product->categories, $categoriesTree);
}
return $this->render('productList.html.twig', ['products' => $products, 'listType' => 'category_' . $category, 'category' => $category, 'categoryTree' => $categoriesTree]);
}
private function addToTree(array $categories, mixed $categoriesTree)
{
$tmp = &$categoriesTree;
foreach ($categories as $category) {
if (empty($tmp[$category])) {
$tmp[$category] = ['count' => 0];
}
$tmp = &$tmp[$category];
$tmp['count']++;
}
return $categoriesTree;
} }
} }

View File

@@ -17,9 +17,10 @@ final class DiscontinuedController extends BaseController
return $this->render('productList.html.twig', ['listType' => 'discontinued']); return $this->render('productList.html.twig', ['listType' => 'discontinued']);
} }
$products = Product::where('lastSeen', '<', now()->format('Y-m-d')) $products = Product::where('updated_at', '<', now()->format('Y-m-d'))
->orderByDesc('starred') ->orderByDesc('starred')
->orderByDesc('created_by') ->orderByDesc('created_by')
->with(['currentPrice', 'lowestPrice'])
->get(); ->get();
return $this->render('productList.html.twig', ['products' => $products, 'listType' => 'discontinued']); return $this->render('productList.html.twig', ['products' => $products, 'listType' => 'discontinued']);
} }

View File

@@ -14,7 +14,7 @@ final class IndexController extends BaseController
if ($this->cache->getItem('list_all')->isHit()) { if ($this->cache->getItem('list_all')->isHit()) {
return $this->render('productList.html.twig', ['listType' => 'all']); return $this->render('productList.html.twig', ['listType' => 'all']);
} }
$products = Product::with(['currentStock']) $products = Product::with(['currentStock', 'price', 'lowestPrice'])
->orderByDesc('starred') ->orderByDesc('starred')
->orderByDesc('created_by') ->orderByDesc('created_by')
->get(); ->get();

View File

@@ -1,29 +0,0 @@
<?php
namespace Krzysiej\RyobiCrawler\Controller;
use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use function Symfony\Component\Clock\now;
final class LowestPriceController extends BaseController
{
#[Route('/lowest-price', name: 'app_lowest_price', methods: ['GET'])]
public function __invoke(): Response
{
$listType = 'lowest_price';
if($this->cache->getItem('lowest_price')->isHit()) {
return $this->render('productList.html.twig', ['listType' => $listType]);
}
$products = Product::whereRaw('priceCurrent = priceLowest')
->whereRaw('lastSeen = "'.now()->format('Y-m-d').'"')
->whereRaw('priceCurrent < productStandardPrice')
->orderByDesc('starred')
->orderByDesc('created_by')
->with(['currentPrice', 'lowestPrice'])
->get();
return $this->render('productList.html.twig', ['products' => $products, 'listType' => $listType]);
}
}

View File

@@ -2,6 +2,7 @@
namespace Krzysiej\RyobiCrawler\Controller; namespace Krzysiej\RyobiCrawler\Controller;
use Illuminate\Database\Eloquent\Builder;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -16,11 +17,11 @@ final class NewController extends BaseController
if($this->cache->getItem('list_new')->isHit()) { if($this->cache->getItem('list_new')->isHit()) {
return $this->render('productList.html.twig', ['listType' => 'new']); return $this->render('productList.html.twig', ['listType' => 'new']);
} }
$date = now()->modify('-30 days')->format('Y-m-d');
$products = Product::where('created_at', '>', $date) $products = Product::where('created_at', '>', now()->modify('-30 days')->format('Y-m-d'))
->with(['country', 'stock'])
->orderByDesc('starred') ->orderByDesc('starred')
->orderByDesc('created_by') ->orderByDesc('created_by')
->with(['currentPrice', 'lowestPrice'])
->get(); ->get();
return $this->render('productList.html.twig', ['products' => $products, 'listType' => 'new']); return $this->render('productList.html.twig', ['products' => $products, 'listType' => 'new']);
} }

View File

@@ -3,6 +3,7 @@
namespace Krzysiej\RyobiCrawler\Controller; namespace Krzysiej\RyobiCrawler\Controller;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
@@ -19,7 +20,7 @@ final class ProductController extends BaseController
#[Route('/product/{productId<\d+>}', name: 'app_product')] #[Route('/product/{productId<\d+>}', name: 'app_product')]
public function __invoke(int $productId): Response public function __invoke(int $productId): Response
{ {
if ($this->cache->getItem('product' . $productId)->isHit()) { if($this->cache->getItem('product'.$productId)->isHit()) {
return $this->render('product.html.twig', ['product' => ['id' => $productId]]); return $this->render('product.html.twig', ['product' => ['id' => $productId]]);
} }
@@ -30,34 +31,26 @@ final class ProductController extends BaseController
if (null === $product) { if (null === $product) {
throw $this->createNotFoundException('Product not found'); throw $this->createNotFoundException('Product not found');
} }
$priceList = $product->price()->pluck('price', 'created_at')->mapWithKeys(fn($price, $createdAt) => [explode(' ', $createdAt)[0] => $price])->toArray(); $priceList = $product->price()->pluck('price');
$stockList = $product->stock()->pluck('stock', 'created_at')->mapWithKeys(fn($stock, $createdAt) => [explode(' ', $createdAt)[0] => $stock])->toArray(); $stockList = $product->stock()->pluck('stock');
$priceDates = $product->price()->pluck('created_at')->map(fn($date) => $date->format('Y-m-d'))->toArray();
$stockDates = $product->stock()->pluck('created_at')->map(fn($date) => $date->format('Y-m-d'))->toArray();
return $this->render('product.html.twig', [ return $this->render('product.html.twig', [
'product' => $product, 'product' => $product,
'price_list' => $this->prepareChartData($priceList), 'price_list' => $this->prepareChartData($priceDates, $priceList),
'stock_list' => $this->prepareChartData($stockList), 'stock_list' => $this->prepareChartData($stockDates, $stockList),
'price_dates' => implode("','", $this->dateRange(array_key_first($priceList), array_key_last($priceList))), 'price_dates' => implode("','", $priceDates),
]); ]);
} }
private function prepareChartData($set1): string private function prepareChartData($set1, $set2): string
{ {
$dates = $this->dateRange(array_key_first($set1), array_key_last($set1)); $data = [];
$data = array_map(fn($date) => ['x' => $date, 'y' => $set1[$date] ?? null], $dates); foreach ($set1 as $key => $value) {
$data[] = ['x' => $value, 'y' => $set2[$key]];
}
$stringData = json_encode($data); $stringData = json_encode($data);
return str_replace(['"x"', '"y"'], ['x', 'y'], $stringData); return str_replace(['"x"', '"y"'], ['x', 'y'], $stringData);
} }
private function dateRange($dateStart, $dateEnd): array
{
$from = new \DateTime($dateStart);
$to = new \DateTime($dateEnd);
$range = [];
for ($date = clone $from; $date < $to; $date->modify('+1 day')) {
$range[] = $date->format('Y-m-d');
}
return $range;
}
} }

View File

@@ -2,6 +2,7 @@
namespace Krzysiej\RyobiCrawler\Controller; namespace Krzysiej\RyobiCrawler\Controller;
use Illuminate\Database\Eloquent\Builder;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -15,7 +16,7 @@ final class PromosController extends BaseController
return $this->render('productList.html.twig', ['listType' => 'promos']); return $this->render('productList.html.twig', ['listType' => 'promos']);
} }
$products = Product::whereRaw('priceCurrent < productStandardPrice') $products = Product::whereHas('currentPrice', fn(Builder $query) => $query->whereColumn('price', '<', 'productStandardPrice'))
->orderByDesc('starred') ->orderByDesc('starred')
->orderByDesc('created_by') ->orderByDesc('created_by')
->with(['currentPrice', 'lowestPrice']) ->with(['currentPrice', 'lowestPrice'])

View File

@@ -12,18 +12,7 @@ final class StarController extends BaseController
#[Route('/star/{productId<\d+>}', name: 'app_star')] #[Route('/star/{productId<\d+>}', name: 'app_star')]
public function __invoke(int $productId, Request $request): Response public function __invoke(int $productId, Request $request): Response
{ {
$this->cache->deleteItems(['list_all', 'list_promos', 'list_new', 'list_discontinued']);
$referer = $request->headers->get('referer');
if (str_contains($referer, '/category/')) {
preg_match('#/category/(.*)#i', $referer, $matches);
$this->cache->deleteItem('list_category_'.urldecode($matches[1]));
}
if (str_contains($referer, '/search?search=')) {
preg_match('#/search\?search=(.*)#i', $referer, $matches);
$this->cache->deleteItem('list_search_'.urldecode($matches[1]));
}
Product::find($productId)->toggleStarred()->save(); Product::find($productId)->toggleStarred()->save();
return $this->redirect($referer); return $this->redirect($request->headers->get('referer'));
} }
} }

View File

@@ -1,24 +0,0 @@
<?php
namespace Krzysiej\RyobiCrawler\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property string $countryName
* @property string $productsUrl
* @property string $cultureCode
* @property string $currency
* @property string $locale
*/
class Country extends Model
{
public $timestamps = true;
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}

View File

@@ -2,10 +2,8 @@
namespace Krzysiej\RyobiCrawler\Models; namespace Krzysiej\RyobiCrawler\Models;
use Carbon\Traits\Date;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
@@ -22,22 +20,11 @@ use function Symfony\Component\Clock\now;
* @property string $modelCode * @property string $modelCode
* @property string $url * @property string $url
* @property int $starred * @property int $starred
* @property float $priceCurrent
* @property float $priceLowest
* @property float $productStandardPrice
* @property float $lowestProductPrice30Days
* @property Date $lastSeen
* @property integer $stock
*/ */
class Product extends Model class Product extends Model
{ {
public $timestamps = true; public $timestamps = true;
public $fillable = ['skuID', 'country_id']; public $fillable = ['skuID'];
public function country(): BelongsTo
{
return $this->belongsTo(Country::class);
}
public function price(): HasMany public function price(): HasMany
{ {
@@ -58,10 +45,6 @@ class Product extends Model
{ {
return $this->hasOne(Price::class)->ofMany('price', 'MIN'); return $this->hasOne(Price::class)->ofMany('price', 'MIN');
} }
public function newestPrice(): HasOne
{
return $this->hasOne(Price::class)->latest();
}
public function stock(): HasMany public function stock(): HasMany
{ {
@@ -70,7 +53,9 @@ class Product extends Model
public function currentStock(): HasOne public function currentStock(): HasOne
{ {
return $this->stock()->one()->ofMany()->withDefault(fn (Stock $stock) => $stock->stock = 0); return $this->stock()->one()->ofMany()->withDefault(function (Stock $stock) {
$stock->stock = 0;
});
} }
public function toggleStarred(): self public function toggleStarred(): self
@@ -90,7 +75,7 @@ class Product extends Model
public function isDiscontinued(): bool public function isDiscontinued(): bool
{ {
return $this->lastSeen < now()->format('Y-m-d'); return $this->updated_at->format('Y-m-d') < now()->format('Y-m-d');
} }
public function isNew(): bool public function isNew(): bool
{ {

View File

@@ -2,11 +2,11 @@
namespace Krzysiej\RyobiCrawler\Twig; namespace Krzysiej\RyobiCrawler\Twig;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Krzysiej\RyobiCrawler\Models\Price; use Krzysiej\RyobiCrawler\Models\Price;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Krzysiej\RyobiCrawler\Models\Stock; use Krzysiej\RyobiCrawler\Models\Stock;
use Symfony\Component\Routing\RouterInterface;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;
use Twig\TwigFunction; use Twig\TwigFunction;
@@ -15,10 +15,6 @@ use function Symfony\Component\Clock\now;
class AppExtension extends AbstractExtension class AppExtension extends AbstractExtension
{ {
public function __construct(public RouterInterface $route)
{
}
public function getFunctions(): array public function getFunctions(): array
{ {
return [ return [
@@ -26,8 +22,6 @@ class AppExtension extends AbstractExtension
new TwigFunction('allCount', [$this, 'allCount']), new TwigFunction('allCount', [$this, 'allCount']),
new TwigFunction('newCount', [$this, 'newCount']), new TwigFunction('newCount', [$this, 'newCount']),
new TwigFunction('discontinuedCount', [$this, 'discontinuedCount']), new TwigFunction('discontinuedCount', [$this, 'discontinuedCount']),
new TwigFunction('lowestPriceCount', [$this, 'lowestPriceCount']),
new TwigFunction('renderCategoryTree', [$this, 'renderCategoryTree']),
]; ];
} }
@@ -37,7 +31,6 @@ class AppExtension extends AbstractExtension
new TwigFilter('findByCreatedAtDate', [$this, 'findByCreatedAtDate']), new TwigFilter('findByCreatedAtDate', [$this, 'findByCreatedAtDate']),
]; ];
} }
public function allCount(): int public function allCount(): int
{ {
return Product::count(); return Product::count();
@@ -45,7 +38,7 @@ class AppExtension extends AbstractExtension
public function promosCount(): int public function promosCount(): int
{ {
return Product::whereRaw('priceCurrent < productStandardPrice')->count(); return Product::whereHas('currentPrice', fn(Builder $query) => $query->whereColumn('price', '<', 'productStandardPrice'))->count();
} }
public function newCount(): int public function newCount(): int
@@ -55,43 +48,11 @@ class AppExtension extends AbstractExtension
public function discontinuedCount(): int public function discontinuedCount(): int
{ {
return Product::where('lastSeen', '<>', now()->format('Y-m-d'))->count(); return Product::where('updated_at', '<', now()->format('Y-m-d'))->count();
}
public function lowestPriceCount(): int
{
return Product::whereRaw('priceCurrent = priceLowest')
->whereRaw('lastSeen = "' . now()->format('Y-m-d') . '"')
->whereRaw('priceCurrent < productStandardPrice')
->count();
} }
public function findByCreatedAtDate(Collection $items, string $date): Stock|Price|null public function findByCreatedAtDate(Collection $items, string $date): Stock|Price|null
{ {
return $items->first(fn($item) => str_starts_with($item->created_at, $date)); return $items->first(fn($item) => str_starts_with($item->created_at, $date));
} }
}
public function renderCategoryTree($categories, $current, $level = 0): string
{
$tree = '';
if ($level == 0) {
$tree .= '<ul class="list-group list-group-flush">';
}
foreach ($categories as $categoryName => $category) {
$currentClass = $categoryName == $current ? 'list-group-item-primary' : '';
$tree .= '<a class="list-group-item list-group-item-action '.$currentClass.' text-decoration-none ms-' . ($level * 2) . '" href="' . $this->route->generate('app_category', ['category' => $categoryName]) . '">' . $categoryName . ' <span class="badge bg-primary rounded-pill">' . $category['count'] . '</span></a>';
unset($category['count']);
if (is_array($category) && count($category) >= 1) {
foreach ($category as $subcategoryName => $subCategory) {
$tree .= $this->renderCategoryTree([$subcategoryName => $subCategory], $current, $level + 1);
}
}
}
if ($level == 0) {
$tree .= '</ul>';
}
return $tree;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,7 @@
<td><img src='{{ product.image }}&width=150' class='border rounded p-1' alt='{{ product.name }}'/></td> <td><img src='{{ product.image }}&width=150' class='border rounded p-1' alt='{{ product.name }}'/></td>
<td> <td>
<a href='{{ path('app_product', {'productId': product.id}) }}' class="text-decoration-none">{{ product.name }}</a> <a href='{{ path('app_product', {'productId': product.id}) }}' class="text-decoration-none">{{ product.name }}</a>
<span class="badge text-bg-light"><a href="{{ path('app_search', {'search': product.subTitle}) }}" class="link-underline link-underline-opacity-0 link-dark">{{ product.subTitle }}</a></span> <span class="badge text-bg-light">{{ product.subTitle }}</span>
</td> </td>
<td> <td>
<nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';"> <nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';">
@@ -42,14 +42,13 @@
<th>Stock</th> <th>Stock</th>
</tr> </tr>
</thead> </thead>
{% set stock = product.stock().get() %}
{% for price in product.price %} {% for price in product.price %}
<tr> <tr>
<td>{{ price.price | format_currency(product.country.currency, {}, product.country.locale) }}</td> <td>{{ price.price | format_currency('PLN', {}, 'pl') }}</td>
<td>{{ price.lowestProductPrice30Days | format_currency(product.country.currency, {}, product.country.locale) }}</td> <td>{{ price.lowestProductPrice30Days | format_currency('PLN', {}, 'pl') }}</td>
<td>{{ price.productStandardPrice | format_currency(product.country.currency, {}, product.country.locale) }}</td> <td>{{ price.productStandardPrice | format_currency('PLN', {}, 'pl') }}</td>
<td>{{ price.created_at }}</td> <td>{{ price.created_at }}</td>
<td>{{ (stock | findByCreatedAtDate(price.created_at | slice(0,10))).stock ?? '' }}</td> <td>{{ (product.stock | findByCreatedAtDate(price.created_at | slice(0,10))).stock ?? '' }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
@@ -68,7 +67,7 @@
labels: ['{{ price_dates|raw }}'], labels: ['{{ price_dates|raw }}'],
datasets: [ datasets: [
{ {
label: 'Price ({{ product.country.currency }})', label: 'Price (PLN)',
data: {{ price_list|raw }}, data: {{ price_list|raw }},
yAxisID: 'yPrice', yAxisID: 'yPrice',
tension: 0.1, tension: 0.1,
@@ -84,7 +83,6 @@
] ]
}, },
options: { options: {
spanGaps: false,
responsive: true, responsive: true,
animation: false, animation: false,
scales: { scales: {
@@ -100,7 +98,7 @@
beginAtZero: true, beginAtZero: true,
title: { title: {
display: true, display: true,
text: 'Price ({{ product.country.currency }})' text: 'Price (PLN)'
}, },
grid: { grid: {
drawOnChartArea: false drawOnChartArea: false

View File

@@ -1,90 +1,65 @@
{% extends "template.html.twig" %} {% extends "template.html.twig" %}
{% block content %} {% block content %}
{% cache 'list_' ~ listType %} {% cache 'list_' ~ listType %}
<div class="table-responsive">
{# <table class='table table-hover'>#}
{# <thead>#}
{# <tr>#}
{# <th></th>#}
{# <th></th>#}
{# <th>Name</th>#}
{# <th>Categories</th>#}
{# <th></th>#}
{# <th class="text-end">Lowest Price</th>#}
{# <th class="text-end">Current Price</th>#}
{# <th></th>#}
{# </tr>#}
{# </thead>#}
{% for product in products %}
<div class="d-inline-flex">
{% if listType starts with 'category_' %} <td class="align-middle font-weight-bold h3"><a class="text-warning text-decoration-none"
{{ renderCategoryTree(categoryTree, category) | raw }} href="{{ path('app_star', {'productId': product.id}) }}">{% if product.starred == true %}{% else %}{% endif %}</a></td>
{% endif %} <td class="align-middle" style="width: 120px;"><img src='{{ product.image }}&width=70' class='img-thumbnail' alt='{{ product.name }}'/></td>
<td class="align-middle">
<a href='{{ path('app_product', {'productId': product.id}) }}' class="text-decoration-none">{{ product.name }}</a>
{% if (listType starts with 'category_' and category == null) or not (listType starts with 'category_') or (listType starts with 'category_' and category is not null) %} {% if product.currentStock.stock > 0 %}
<div class="table-responsive"> <span class="badge text-bg-light">stock: {{ product.currentStock.stock }}</span>
<table class='table table-hover'> {% else %}
<thead> <span class="badge text-bg-warning">out of stock</span>
<tr> {% endif %}
<th></th> {% if product.isDiscontinued() %}
<th></th> <span class="badge text-bg-secondary" data-bs-toggle="tooltip" data-bs-title="Last update: {{ product.updated_at }}">is discontinued</span>
<th>Name</th> {% endif %}
<th>Categories</th> {% if product.isNew() %}
<th></th> <span class="badge text-bg-success">is new</span>
<th class="text-end">Lowest Price</th> {% endif %}
<th class="text-end">Current Price</th> <span class="badge text-bg-light">{{ product.subTitle }}</span>
<th></th> </td>
</tr> <td class="align-middle">
</thead> <nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';" >
{% for product in products %} <ol class="breadcrumb mb-0">
<tr> {% for category in product.categories %}
<td class="align-middle font-weight-bold h3"><a class="text-warning text-decoration-none" <li class="breadcrumb-item" aria-current="page"><a class="breadcrumb-item text-decoration-none" href="{{ path('app_category', {'category': category}) }}">{{ category }}</a></li>
href="{{ path('app_star', {'productId': product.id}) }}">{% if product.starred == true %}{% else %}{% endif %}</a> {% endfor %}
</td> </ol>
<td class="align-middle" style="width: 120px;"><img src='{{ product.image }}&width=70' </nav>
class='img-thumbnail' </td>
alt='{{ product.name }}'/></td> <td class="align-middle"><a href='https://pl.ryobitools.eu/{{ product.url }}'>link</a></td>
<td class="align-middle"> <td class="align-middle text-end">{% if product.lowestPrice.price != product.price.last.price %}{{ product.lowestPrice.price | format_currency('PLN', {}, 'pl') }}{% endif %}</td>
<a href='{{ path('app_product', {'productId': product.id}) }}' <td class="align-middle text-end">{{ product.price.last.price | format_currency('PLN', {}, 'pl') }}</td>
class="text-decoration-none">{{ product.name }}</a> <td class="align-middle">
{% if product.stock > 0 %} <div class="d-flex flex-row">
<span class="badge text-bg-light">stock: {{ product.stock }}</span> {% if product.price.last.price != product.price.last.productStandardPrice %}<span
{% else %} class="badge text-bg-warning text-decoration-line-through flex-fill">{{ product.price.last.productStandardPrice | format_currency('PLN', {}, 'pl') }}</span> <span
<span class="badge text-bg-warning">out of stock</span> class="badge text-bg-success flex-fill">{{ ((1 - product.price.last.price / product.price.last.productStandardPrice)*100)|number_format(0) }}%</span>
{% endif %} {% endif %}
{% if product.isDiscontinued() %} </div>
<span class="badge text-bg-secondary" data-bs-toggle="tooltip" </td>
data-bs-title="Last update: {{ product.lastSeen }}">is discontinued</span>
{% endif %}
{% if product.isNew() %}
<span class="badge text-bg-success">is new</span>
{% endif %}
<span class="badge text-bg-light"><a
href="{{ path('app_search', {'search': product.subTitle}) }}"
class="link-underline link-underline-opacity-0 link-dark">{{ product.subTitle }}</a></span>
</td>
<td class="align-middle">
<nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';">
<ol class="breadcrumb mb-0">
{% for category in product.categories %}
<li class="breadcrumb-item" aria-current="page"><a
class="breadcrumb-item text-decoration-none"
href="{{ path('app_category', {'category': category}) }}">{{ category }}</a>
</li>
{% endfor %}
</ol>
</nav>
</td>
<td class="align-middle"><a href='https://pl.ryobitools.eu/{{ product.url }}'>link</a></td>
<td class="align-middle text-end">
{% if product.isDiscontinued() or product.priceCurrent == product.productStandardPrice %}
{{ product.priceLowest | format_currency(product.country.currency, {}, product.country.locale) }}
{% else %}
{% if product.priceLowest != product.priceCurrent %}{{ product.priceLowest | format_currency(product.country.currency, {}, product.country.locale) }}{% else %}
<span class="badge text-bg-info">now lowest</span>{% endif %}</td>
{% endif %}
<td class="align-middle text-end">{{ product.priceCurrent | format_currency(product.country.currency, {}, product.country.locale) }}</td>
<td class="align-middle">
<div class="d-flex flex-row">
{% if product.priceCurrent != product.productStandardPrice %}<span
class="badge text-bg-warning text-decoration-line-through flex-fill">{{ product.productStandardPrice | format_currency(product.country.currency, {}, product.country.locale) }}</span>
<span
class="badge text-bg-success flex-fill">{{ ((1 - product.priceCurrent / product.productStandardPrice)*100)|number_format(0) }}%</span>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>
</div> </div>
{% endif %} {% endfor %}
{% endcache %} {# </table>#}
</div>
{% endcache %}
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en" data-bs-theme="light"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
@@ -18,16 +18,13 @@
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
{% cache "menu_count" ~ listType|default('') %} {% cache "menu_count" %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if app.request.pathinfo == path('app_home') %}active shadow-sm bg-body rounded{% endif %}" aria-current="page" href="{{ path('app_home') }}">All products <span class="badge text-bg-secondary">{{ allCount() }}</span></a> <a class="nav-link {% if app.request.pathinfo == path('app_home') %}active shadow-sm bg-body rounded{% endif %}" aria-current="page" href="{{ path('app_home') }}">All products <span class="badge text-bg-secondary">{{ allCount() }}</span></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if app.request.pathinfo == path('app_promos') %}active shadow-sm bg-body rounded{% endif %}" aria-current="page" href="{{ path('app_promos') }}">Promos <span class="badge text-bg-secondary">{{ promosCount() }}</span></a> <a class="nav-link {% if app.request.pathinfo == path('app_promos') %}active shadow-sm bg-body rounded{% endif %}" aria-current="page" href="{{ path('app_promos') }}">Promos <span class="badge text-bg-secondary">{{ promosCount() }}</span></a>
</li> </li>
<li class="nav-item">
<a class="nav-link {% if app.request.pathinfo == path('app_lowest_price') %}active shadow-sm bg-body rounded{% endif %}" aria-current="page" href="{{ path('app_lowest_price') }}">Lowest price <span class="badge text-bg-secondary">{{ lowestPriceCount() }}</span></a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if app.request.pathinfo == path('app_new') %}active shadow-sm bg-body rounded{% endif %}" aria-current="page" href="{{ path('app_new') }}">New in last 30 days <span class="badge text-bg-secondary">{{ newCount() }}</span></a> <a class="nav-link {% if app.request.pathinfo == path('app_new') %}active shadow-sm bg-body rounded{% endif %}" aria-current="page" href="{{ path('app_new') }}">New in last 30 days <span class="badge text-bg-secondary">{{ newCount() }}</span></a>
</li> </li>
@@ -47,7 +44,8 @@
</nav> </nav>
{% block content %}{% endblock %} {% block content %}{% endblock %}
<script src="/templates/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body> </body>
</html> </html>