13 Commits

Author SHA1 Message Date
0da1aa4ff6 Country stats and searching by exact model name.
All checks were successful
/ deploy-job (push) Successful in 0s
2026-02-24 08:44:23 +01:00
59f10e620f Add country name when filtering by country name
All checks were successful
/ deploy-job (push) Successful in 0s
2026-02-23 08:37:12 +01:00
9f322d874e Add link to now lowest badge
All checks were successful
/ deploy-job (push) Successful in 0s
2026-02-23 08:29:46 +01:00
10cd2c59a3 Add countries selector
All checks were successful
/ deploy-job (push) Successful in 1s
2026-02-22 09:37:19 +01:00
810643c18a Start working on filtering by country
All checks were successful
/ deploy-job (push) Successful in 0s
2026-02-21 08:40:07 +01:00
ad0ef20fc2 Country badge.
All checks were successful
/ deploy-job (push) Successful in 1s
2026-02-19 09:33:56 +01:00
e68389bccc Add locale badges to promos 2026-02-19 09:33:56 +01:00
e1dbdc539b Badges as links 2026-02-19 09:33:56 +01:00
f0f26ad64d Badges as links 2026-02-19 09:33:56 +01:00
c6c11d5f7f Merge pull request 'Add the rest of available countries.' (#48) from feature/new-countries into master
All checks were successful
/ deploy-job (push) Successful in 0s
2026-02-17 09:34:55 +01:00
7373657d78 Merge branch 'refs/heads/master' into feature/new-countries
All checks were successful
/ deploy-job (push) Successful in 0s
# Conflicts:
#	src/Command/Migrate.php
#	templates/product.html.twig
#	templates/productList.html.twig
2026-02-17 09:32:38 +01:00
3bbf82f897 Start working on handling multiple countries at once 2026-01-19 08:40:01 +01:00
a388b53cff Add the rest of available countries. 2026-01-19 08:33:19 +01:00
7 changed files with 154 additions and 3 deletions

View File

@@ -118,6 +118,66 @@ class Migrate extends Command
]
);
}
if (Capsule::schema()->hasTable('countries') && !Country::where('countryName', 'Netherlands')->exists()) {
Capsule::table('countries')->insert([
'countryName' => 'Netherlands',
'productsUrl' => 'https://nl.ryobitools.eu/api/product-listing/get-products',
'cultureCode' => 'nl-NL',
'currency' => 'EUR',
'locale' => 'nl',
'created_at' => now(),
'updated_at' => now(),
]
);
}
if (Capsule::schema()->hasTable('countries') && !Country::where('countryName', 'France')->exists()) {
Capsule::table('countries')->insert([
'countryName' => 'France',
'productsUrl' => 'https://fr.ryobitools.eu/api/product-listing/get-products',
'cultureCode' => 'fr-FR',
'currency' => 'EUR',
'locale' => 'fr',
'created_at' => now(),
'updated_at' => now(),
]
);
}
if (Capsule::schema()->hasTable('countries') && !Country::where('countryName', 'Spain')->exists()) {
Capsule::table('countries')->insert([
'countryName' => 'Spain',
'productsUrl' => 'https://es.ryobitools.eu/api/product-listing/get-products',
'cultureCode' => 'es-ES',
'currency' => 'EUR',
'locale' => 'es',
'created_at' => now(),
'updated_at' => now(),
]
);
}
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' => 'uk',
'created_at' => now(),
'updated_at' => now(),
]
);
}
if (!Capsule::schema()->hasColumn('products', 'country_id')) {
Capsule::schema()->table('products', function (Blueprint $table) use ($id) {

View File

@@ -0,0 +1,40 @@
<?php
namespace Krzysiej\RyobiCrawler\Controller;
use Illuminate\Database\Eloquent\Builder;
use Krzysiej\RyobiCrawler\Models\Country;
use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class CountryController extends BaseController
{
#[Route('/country/{countryName?}', name: 'app_country')]
public function __invoke(?string $countryName): Response
{
/** @var Product[] $products */
$products = Product::with(['price', 'lowestPrice', 'country'])
->join('countries', 'countries.id', '=', 'products.country_id')
->where('countryName', $countryName)
->orderByDesc('starred')
->orderByDesc('created_by')
->get();
$country = Country::where('countryName', $countryName)->first();
return $this->render('productList.html.twig', ['products' => $products, 'listType' => 'country', 'country' => $country, 'countryStats' => $this->countryStats($countryName)]);
}
private function countryStats(string $country): array
{
$countryProducts = Product::join('countries', 'countries.id', '=', 'products.country_id')
->where('countryName', $country);
$stats = [];
$stats['items'] = $countryProducts->get()->count();
// $stats['items2'] = $countryProducts->dd();
return $stats;
}
}

View File

@@ -32,6 +32,32 @@ final class ProductController extends BaseController
ksort($stockList);
ksort($priceList);
return $this->render('product.html.twig', [
'product' => $product,
'price_list' => $this->prepareChartData($priceList),
'stock_list' => $this->prepareChartData($stockList),
'price_dates' => implode("','", $this->dateRange(array_key_first($priceList), array_key_last($priceList))),
]);
}
#[Route('/product/model/{productModel}', name: 'app_product_model')]
public function productModel(string $productModel): Response
{
$product = Product::with([
'price' => fn($query) => $query->orderBy('created_at', 'desc'),
'stock' => fn($query) => $query->orderBy('created_at', 'desc'),
])->where('subTitle', $productModel)->first();
if (null === $product) {
throw $this->createNotFoundException('Product not found');
}
$priceList = $product->price()->pluck('price', 'created_at')->mapWithKeys(fn($price, $createdAt) => [explode(' ', $createdAt)[0] => $price])->toArray();
$stockList = $product->stock()->pluck('stock', 'created_at')->mapWithKeys(fn($stock, $createdAt) => [explode(' ', $createdAt)[0] => $stock])->toArray();
ksort($stockList);
ksort($priceList);
return $this->render('product.html.twig', [
'product' => $product,
'price_list' => $this->prepareChartData($priceList),

View File

@@ -3,6 +3,7 @@
namespace Krzysiej\RyobiCrawler\Twig;
use Illuminate\Database\Eloquent\Collection;
use Krzysiej\RyobiCrawler\Models\Country;
use Krzysiej\RyobiCrawler\Models\Price;
use Krzysiej\RyobiCrawler\Models\Product;
use Krzysiej\RyobiCrawler\Models\Stock;
@@ -28,6 +29,7 @@ class AppExtension extends AbstractExtension
new TwigFunction('discontinuedCount', [$this, 'discontinuedCount']),
new TwigFunction('lowestPriceCount', [$this, 'lowestPriceCount']),
new TwigFunction('renderCategoryTree', [$this, 'renderCategoryTree']),
new TwigFunction('getCountries', [$this, 'getCountries']),
];
}
@@ -66,6 +68,11 @@ class AppExtension extends AbstractExtension
->count();
}
public function getCountries(): array
{
return Country::get()->toArray();
}
public function findByCreatedAtDate(Collection $items, string $date): Stock|Price|null
{
return $items->first(fn($item) => str_starts_with($item->created_at, $date));

View File

@@ -11,7 +11,7 @@
<td>
<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}) }}"
<span class="badge text-bg-light"><a href="{{ path('app_product_model', {'productModel': product.subTitle}) }}"
class="link-underline link-underline-opacity-0 link-dark">{{ product.subTitle }}</a></span>
{% if product.promotions is not null and product.promotions.hasPromotion %}<a
href="{{ path('app_promos', {'promo': product.promotions.slug}) }}"><span class="badge bg-info">PROMO: {{ product.promotions.tag }}</span>

View File

@@ -19,6 +19,12 @@
</ul>
{% endif %}
{% if listType == 'country' and country is not null%}
<h2 class="text-muted mt-4 mx-4">{{ country.countryName }}</h2>
<h5 class="mx-4 my-0">Currency: {{ country.currency }}</h5>
{{ dump(countryStats) }}
{% endif %}
{% if (listType starts with 'category_' and category == null) or not (listType starts with 'category_') or (listType starts with 'category_' and category is not null) %}
<div class="table-responsive">
@@ -46,6 +52,7 @@
<td class="align-middle">
<a href='{{ path('app_product', {'productId': product.id}) }}'
class="text-decoration-none">{{ product.name }}</a>
<br>
{% if product.stock > 0 %}
<span class="badge text-bg-light">stock: {{ product.stock }}</span>
{% else %}
@@ -59,9 +66,10 @@
<a href="{{ path('app_new') }}"><span class="badge text-bg-success">is new</span></a>
{% endif %}
<span class="badge text-bg-light"><a
href="{{ path('app_search', {'search': product.subTitle}) }}"
href="{{ path('app_product_model', {'productModel': product.subTitle}) }}"
class="link-underline link-underline-opacity-0 link-dark">{{ product.subTitle }}</a></span>
{% if product.promotions is not null and product.promotions.hasPromotion %}<a href="{{ path('app_promos', {'promo': product.promotions.slug}) }}"><span class="badge bg-info">PROMO: {{ product.promotions.tag }}</span></a>{% endif %}
<span class="badge text-bg-light"><a href="{{ path('app_country', {'countryName': product.country.countryName}) }}" class="link-underline link-underline-opacity-0 link-dark">{{ product.country.countryName }}</a></span>
</td>
<td class="align-middle">
<nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';">
@@ -81,7 +89,7 @@
{{ 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>
<a href="{{ path('app_lowest_price') }}"><span class="badge bg-info">now lowest</span></a>{% endif %}</td>
{% endif %}
<td class="align-middle text-end">{{ product.priceCurrent | format_currency(product.country.currency, {}, product.country.locale) }}</td>

View File

@@ -33,6 +33,16 @@
<li class="nav-item">
<a class="nav-link {% if app.request.pathinfo == path('app_discontinued') %}active shadow-sm bg-body rounded{% endif %}" aria-current="page" href="{{ path('app_discontinued') }}">Discontinued <span class="badge text-bg-secondary">{{ discontinuedCount() }}</span></a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Countries
</a>
<ul class="dropdown-menu">
{% for country in getCountries() %}
<li><a class="dropdown-item" href="{{ path('app_country', {'countryName': country.countryName}) }}">{{ country.countryName }}</a></li>
{% endfor %}
</ul>
</li>
</ul>
<form class="form-floating d-flex col-lg-6 col-sm-8" role="search" action="{{ path('app_search') }}">