Compare commits
3 Commits
fc89944cf1
...
feature/ha
| Author | SHA1 | Date | |
|---|---|---|---|
| e02fa4fc67 | |||
| 5088f6173f | |||
| 09825de7b9 |
@@ -1,7 +1,5 @@
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
deploy-job:
|
deploy-job:
|
||||||
|
|||||||
@@ -246,6 +246,16 @@ class Migrate extends Command
|
|||||||
$table->json('promotions')->nullable();
|
$table->json('promotions')->nullable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!Capsule::schema()->hasColumn('prices', 'conversionRate')) {
|
||||||
|
Capsule::schema()->table('prices', function (Blueprint $table) {
|
||||||
|
$table->float('conversionRate')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Capsule::schema()->hasColumn('products', 'conversionRate')) {
|
||||||
|
Capsule::schema()->table('products', function (Blueprint $table) {
|
||||||
|
$table->float('conversionRate')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index(): void
|
public function index(): void
|
||||||
|
|||||||
@@ -23,15 +23,17 @@ class ScrapeWebsite extends Command
|
|||||||
{
|
{
|
||||||
const COUNTRY_ID = 'country';
|
const COUNTRY_ID = 'country';
|
||||||
private Client $client;
|
private Client $client;
|
||||||
|
private array $rates;
|
||||||
|
|
||||||
public function __construct(protected Capsule $database)
|
public function __construct(protected Capsule $database)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
$this->client = new Client();
|
||||||
|
$this->rates = $this->getCurrencyExchange();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->client = new Client();
|
|
||||||
$this->addOption(self::COUNTRY_ID, 'c', InputOption::VALUE_OPTIONAL, 'Country id');
|
$this->addOption(self::COUNTRY_ID, 'c', InputOption::VALUE_OPTIONAL, 'Country id');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +70,7 @@ class ScrapeWebsite extends Command
|
|||||||
$product->priceLowest = $product->lowestPrice->price;
|
$product->priceLowest = $product->lowestPrice->price;
|
||||||
$product->lastSeen = $newestPrice->created_at->format('Y-m-d');
|
$product->lastSeen = $newestPrice->created_at->format('Y-m-d');
|
||||||
$product->stock = $currentStock->stock;
|
$product->stock = $currentStock->stock;
|
||||||
|
$product->conversionRate = $newestPrice->conversionRate;
|
||||||
$product->save(['timestamps' => false]);
|
$product->save(['timestamps' => false]);
|
||||||
$progress->advance();
|
$progress->advance();
|
||||||
}
|
}
|
||||||
@@ -98,7 +101,7 @@ class ScrapeWebsite extends Command
|
|||||||
$products = array_merge($products, $responseObject->products);
|
$products = array_merge($products, $responseObject->products);
|
||||||
$page++;
|
$page++;
|
||||||
$canLoadMore = $responseObject->canLoadMore;
|
$canLoadMore = $responseObject->canLoadMore;
|
||||||
} catch (GuzzleException $e) {
|
} catch (GuzzleException) {
|
||||||
return $products;
|
return $products;
|
||||||
}
|
}
|
||||||
} while ($canLoadMore);
|
} while ($canLoadMore);
|
||||||
@@ -132,6 +135,7 @@ class ScrapeWebsite extends Command
|
|||||||
$price->price = $product->productPrice;
|
$price->price = $product->productPrice;
|
||||||
$price->productStandardPrice = $product->productStandardPrice;
|
$price->productStandardPrice = $product->productStandardPrice;
|
||||||
$price->lowestProductPrice30Days = $product->lowestProductPrice30Days;
|
$price->lowestProductPrice30Days = $product->lowestProductPrice30Days;
|
||||||
|
$price->conversionRate = $this->getConversionRate($country->currency);
|
||||||
$productModel->price()->save($price);
|
$productModel->price()->save($price);
|
||||||
}
|
}
|
||||||
$stockExist = $productModel->stock()->whereRaw("strftime('%Y-%m-%d', created_at) = ?", [date('Y-m-d')])->exists();
|
$stockExist = $productModel->stock()->whereRaw("strftime('%Y-%m-%d', created_at) = ?", [date('Y-m-d')])->exists();
|
||||||
@@ -147,4 +151,22 @@ class ScrapeWebsite extends Command
|
|||||||
$productModel->stock()->save($stock);
|
$productModel->stock()->save($stock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCurrencyExchange(): array
|
||||||
|
{
|
||||||
|
$result = $this->client->request('GET', 'https://api.nbp.pl/api/exchangerates/tables/A/?format=json');
|
||||||
|
$rates = ['PLN' => 1.0];
|
||||||
|
foreach(json_decode($result->getBody()->getContents(),true)[0]['rates'] as $rate){
|
||||||
|
$rates[$rate['code']] = $rate['mid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getConversionRate(string $currency): float
|
||||||
|
{
|
||||||
|
$currency = strtoupper($currency);
|
||||||
|
|
||||||
|
return $this->rates[$currency];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,13 @@ use Krzysiej\RyobiCrawler\Twig\AppExtension;
|
|||||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||||
use Symfony\Bundle\TwigBundle\TwigBundle;
|
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||||
|
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||||
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||||
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
|
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
|
||||||
|
use Twig\Extra\Cache\CacheExtension;
|
||||||
|
use Twig\Extra\Cache\CacheRuntime;
|
||||||
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
|
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
|
||||||
|
|
||||||
class Kernel extends BaseKernel
|
class Kernel extends BaseKernel
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
* @property float $price
|
* @property float $price
|
||||||
* @property float $productStandardPrice
|
* @property float $productStandardPrice
|
||||||
* @property float $lowestProductPrice30Days
|
* @property float $lowestProductPrice30Days
|
||||||
|
* @property float $conversionRate
|
||||||
*/
|
*/
|
||||||
class Price extends Model
|
class Price extends Model
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use function Symfony\Component\Clock\now;
|
|||||||
* @property float $priceLowest
|
* @property float $priceLowest
|
||||||
* @property float $productStandardPrice
|
* @property float $productStandardPrice
|
||||||
* @property float $lowestProductPrice30Days
|
* @property float $lowestProductPrice30Days
|
||||||
|
* @property float $conversionRate
|
||||||
* @property Date $lastSeen
|
* @property Date $lastSeen
|
||||||
* @property integer $stock
|
* @property integer $stock
|
||||||
* @property Object $promotions
|
* @property Object $promotions
|
||||||
@@ -103,6 +104,11 @@ class Product extends Model
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function conversionRate(): ?float
|
||||||
|
{
|
||||||
|
return $this->conversionRate;
|
||||||
|
}
|
||||||
|
|
||||||
public function isDiscontinued(): bool
|
public function isDiscontinued(): bool
|
||||||
{
|
{
|
||||||
return $this->lastSeen < now()->format('Y-m-d');
|
return $this->lastSeen < now()->format('Y-m-d');
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
|
|
||||||
<div class="col-md-4 d-flex align-items-center">
|
|
||||||
<span class="mb-3 mb-md-0 text-body-secondary">© 2025 Company, Inc</span>
|
|
||||||
<a class="align-items-center text-decoration-none" data-bs-theme-value="light">☀️ Light</a>
|
|
||||||
<a class="align-items-center text-decoration-none" data-bs-theme-value="dark">🌕 Dark</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
<td>
|
<td>
|
||||||
<a href='{{ path('app_product', {'productId': product.id}) }}'
|
<a href='{{ path('app_product', {'productId': product.id}) }}'
|
||||||
class="text-decoration-none">{{ product.name }}</a>
|
class="text-decoration-none">{{ product.name }}</a>
|
||||||
<a href="{{ path('app_search', {'search': product.subTitle}) }}"><span class="badge text-bg-secondary">{{ product.subTitle }}</span></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>
|
||||||
{% if product.promotions is not null and product.promotions.hasPromotion %}<a
|
{% 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>
|
href="{{ path('app_promos', {'promo': product.promotions.slug}) }}"><span class="badge bg-info">PROMO: {{ product.promotions.tag }}</span>
|
||||||
</a>{% endif %}
|
</a>{% endif %}
|
||||||
|
|||||||
@@ -48,19 +48,22 @@
|
|||||||
class="text-decoration-none">{{ product.name }}</a>
|
class="text-decoration-none">{{ product.name }}</a>
|
||||||
<br>
|
<br>
|
||||||
{% if product.stock > 0 %}
|
{% if product.stock > 0 %}
|
||||||
<span class="badge text-bg-secondary">stock: {{ product.stock }}</span>
|
<span class="badge text-bg-light">stock: {{ product.stock }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge text-bg-warning">out of stock</span>
|
<span class="badge text-bg-warning">out of stock</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if product.isDiscontinued() %}
|
{% if product.isDiscontinued() %}
|
||||||
<a href="{{ path('app_discontinued') }}"><span class="badge text-bg-secondary" data-bs-title="Last update: {{ product.lastSeen }}">is discontinued</span></a>
|
<a href="{{ path('app_discontinued') }}"><span class="badge text-bg-secondary" data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="Last update: {{ product.lastSeen }}">is discontinued</span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if product.isNew() %}
|
{% if product.isNew() %}
|
||||||
<a href="{{ path('app_new') }}"><span class="badge text-bg-success">is new</span></a>
|
<a href="{{ path('app_new') }}"><span class="badge text-bg-success">is new</span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ path('app_search', {'search': product.subTitle}) }}"><span class="badge text-bg-secondary">{{ product.subTitle }}</span></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>
|
||||||
{% 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 %}
|
{% 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-secondary">{{ product.country.countryName }}</span>
|
<span class="badge text-bg-light">{{ product.country.countryName }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';">
|
<nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';">
|
||||||
@@ -77,19 +80,30 @@
|
|||||||
<td class="align-middle"><a href='https://{{ product.country.locale }}.ryobitools.eu{{ product.url }}'>link</a></td>
|
<td class="align-middle"><a href='https://{{ product.country.locale }}.ryobitools.eu{{ product.url }}'>link</a></td>
|
||||||
<td class="align-middle text-end">
|
<td class="align-middle text-end">
|
||||||
{% if product.isDiscontinued() or product.priceCurrent == product.productStandardPrice %}
|
{% if product.isDiscontinued() or product.priceCurrent == product.productStandardPrice %}
|
||||||
{{ product.priceLowest | format_currency(product.country.currency, {}, product.country.locale) }}
|
{{ product.priceLowest | format_currency(product.country.currency, {}, product.country.locale) }}<br>
|
||||||
|
{% if product.conversionRate is not empty and product.conversionRate != 1 %}
|
||||||
|
{{ (product.priceLowest * product.conversionRate) | format_currency('PLN', {}, 'pl') }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if product.priceLowest != product.priceCurrent %}{{ 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>
|
<span class="badge text-bg-info">now lowest</span>
|
||||||
|
{% endif %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<td class="align-middle text-end">{{ product.priceCurrent | format_currency(product.country.currency, {}, product.country.locale) }}</td>
|
<td class="align-middle text-end">
|
||||||
|
{{ product.priceCurrent | format_currency(product.country.currency, {}, product.country.locale) }}<br>
|
||||||
|
{% if product.conversionRate is not empty and product.conversionRate != 1 %}
|
||||||
|
{{ (product.priceCurrent * product.conversionRate) | format_currency('PLN', {}, 'pl') }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
{% if product.priceCurrent != product.productStandardPrice %}<span
|
{% if product.priceCurrent != product.productStandardPrice %}
|
||||||
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-warning text-decoration-line-through flex-fill">{{ product.productStandardPrice | format_currency(product.country.currency, {}, product.country.locale) }}</span>
|
||||||
<span
|
{% if product.conversionRate is not empty and product.conversionRate != 1 %}
|
||||||
class="badge text-bg-success flex-fill">{{ ((1 - product.priceCurrent / product.productStandardPrice)*100)|number_format(0) }}%</span>
|
<span class="badge text-bg-warning text-decoration-line-through flex-fill">{{ (product.productStandardPrice * product.conversionRate) | format_currency('PLN', {}, 'pl') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="badge text-bg-success flex-fill">{{ ((1 - product.priceCurrent / product.productStandardPrice)*100)|number_format(0) }}%</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,75 +1,6 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-bs-theme="">
|
<html lang="en" data-bs-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<script>
|
|
||||||
(() => {
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const getStoredTheme = () => localStorage.getItem('theme')
|
|
||||||
const setStoredTheme = theme => localStorage.setItem('theme', theme)
|
|
||||||
|
|
||||||
const getPreferredTheme = () => {
|
|
||||||
const storedTheme = getStoredTheme()
|
|
||||||
if (storedTheme) {
|
|
||||||
return storedTheme
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
||||||
}
|
|
||||||
|
|
||||||
const setTheme = theme => {
|
|
||||||
if (theme === 'auto') {
|
|
||||||
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
|
|
||||||
} else {
|
|
||||||
document.documentElement.setAttribute('data-bs-theme', theme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTheme(getPreferredTheme())
|
|
||||||
|
|
||||||
const showActiveTheme = (theme, focus = false) => {
|
|
||||||
const themeSwitcher = document.querySelector('#bd-theme')
|
|
||||||
|
|
||||||
if (!themeSwitcher) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
|
|
||||||
element.classList.remove('active')
|
|
||||||
})
|
|
||||||
|
|
||||||
btnToActive.classList.add('active')
|
|
||||||
|
|
||||||
if (focus) {
|
|
||||||
themeSwitcher.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
||||||
const storedTheme = getStoredTheme()
|
|
||||||
if (storedTheme !== 'light' && storedTheme !== 'dark') {
|
|
||||||
setTheme(getPreferredTheme())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
|
||||||
showActiveTheme(getPreferredTheme())
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-bs-theme-value]')
|
|
||||||
.forEach(toggle => {
|
|
||||||
toggle.addEventListener('click', () => {
|
|
||||||
const theme = toggle.getAttribute('data-bs-theme-value')
|
|
||||||
setStoredTheme(theme)
|
|
||||||
setTheme(theme)
|
|
||||||
showActiveTheme(theme, true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})()
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<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">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
@@ -82,8 +13,7 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="{{ path('app_home') }}">Crawler</a>
|
<a class="navbar-brand" href="{{ path('app_home') }}">Crawler</a>
|
||||||
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
@@ -115,7 +45,6 @@
|
|||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
{{ include('footer.html.twig') }}
|
|
||||||
<script src="/templates/js/bootstrap.bundle.min.js"></script>
|
<script src="/templates/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user