Skip to content

Commit c5095d1

Browse files
committed
start building dashboard with widgets
add global search hotkey add community stores where users can import/export stores from new repository fix media markt 404 on crawling add currency resource, so users can modify the rate without the need for API add new '*' in case there are multiple fields in schema add basic code for plugin support to test it in production, shouldn't affect anything add warning message if ntfy is added without topic
1 parent a4fa59e commit c5095d1

File tree

31 files changed

+2542
-28
lines changed

31 files changed

+2542
-28
lines changed

app/Classes/CustomStoreTemplate.php

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Illuminate\Support\Facades\DB;
1919
use Illuminate\Support\Facades\Log;
2020
use Illuminate\Support\Str;
21+
use Illuminate\Support\Uri;
2122

2223
class CustomStoreTemplate
2324
{
@@ -350,11 +351,20 @@ public function get_price()
350351
return;
351352
}
352353

353-
$results = $this->dom->querySelectorAll($this->link->store->custom_settings['price_selectors']);
354+
if (filled($this->link->store->custom_settings['price_selectors'])) {
355+
$results = $this->dom->querySelectorAll($this->link->store->custom_settings['price_selectors']);
356+
$attributes = ['content'];
354357

355-
$attributes = ['content'];
358+
$this->get_results_for_key($results, $attributes, 'price');
359+
360+
$this->product_data['price'] = (float) GeneralHelper::get_numbers_with_normalized_format(
361+
GeneralHelper::get_numbers_only_with_dot_and_comma($this->product_data['price'])
362+
);
363+
364+
}
356365

357-
$this->get_results_for_key($results, $attributes, 'price');
366+
if (! $this->product_data['price'] && $this->link->store->custom_settings['price_schema_key'])
367+
$this->product_data['price'] = $this->search_for_custom_keys_and_get_values($this->link->store->custom_settings['price_schema_key']);
358368

359369
$this->product_data['price'] = (float) GeneralHelper::get_numbers_with_normalized_format(
360370
GeneralHelper::get_numbers_only_with_dot_and_comma($this->product_data['price'])
@@ -453,4 +463,60 @@ public function update_link_history()
453463

454464
$history->save();
455465
}
466+
467+
468+
public function search_for_custom_keys_and_get_values(string $key, array $current_schema = [])
469+
{
470+
if (blank($key) || blank($this->schema)) return null;
471+
472+
$key_parts = explode('.', $key);
473+
474+
$remaining_key_parts = $key_parts;
475+
476+
$shallow_schema = $current_schema ?: $this->schema;
477+
478+
// go through each key part and search the parth for it until we get a result
479+
foreach ($key_parts as &$key_part) {
480+
481+
$remaining_key_parts = array_slice($remaining_key_parts, 1);
482+
483+
// check for case [sku=value]
484+
if (preg_match('/^\[[^=]+=[^]]+\]$/', $key_part)) {
485+
486+
// check the schema key we want to access to compare its value to the value of get param of schema_key_value
487+
[$schema_key, $schema_key_value] = Str::of($key_part)
488+
->between('[', ']')
489+
->explode('=');
490+
491+
$value_to_search = Uri::of($this->current_product_url)
492+
->query()
493+
->get($schema_key_value);
494+
495+
// if they are not the same, skip the current record, otherwise continue to the next key.
496+
if (strtolower($current_schema[$schema_key]) != strtolower($value_to_search))
497+
return null;
498+
499+
continue;
500+
}
501+
502+
if ($key_part == '*') {
503+
// go through every record and return the first path that returns a value
504+
foreach (data_get($shallow_schema, '*') as $single_record) {
505+
$value = $this->search_for_custom_keys_and_get_values(implode('.', $remaining_key_parts), $single_record);
506+
507+
if (filled($value))
508+
return $value;
509+
}
510+
}
511+
512+
if (! isset($shallow_schema[$key_part]))
513+
return null;
514+
515+
// check if they key exists to go through it, if we reach to a point where there isn't a child, return the result back
516+
$shallow_schema = $shallow_schema[$key_part];
517+
}
518+
519+
return $shallow_schema;
520+
}
521+
456522
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
3+
namespace App\Filament\Forms;
4+
5+
use App\Helpers\StoreHelper;
6+
use App\Jobs\AddStoreToDiscountJob;
7+
use Daikazu\FilamentImageCheckboxGroup\Forms\Components\ImageCheckboxGroup;
8+
use Filament\Actions\Action;
9+
use Filament\Forms\Components\Hidden;
10+
use Filament\Forms\Components\TextInput;
11+
use Filament\Forms\Components\ToggleButtons;
12+
use Filament\Notifications\Notification;
13+
use Filament\Schemas\Components\Wizard;
14+
use Filament\Schemas\Components\Wizard\Step;
15+
use Filament\Support\Icons\Heroicon;
16+
use Illuminate\Support\Facades\Auth;
17+
18+
/**
19+
* Community Stores Form Configuration
20+
*
21+
* This class provides a Filament form wizard that allows users to search and add community stores
22+
* to their discount collection. The wizard consists of two main steps:
23+
* 1. Search and select stores from the community store repository
24+
* 2. Select specific domains for the chosen stores
25+
*
26+
* The form integrates with GitHub-hosted community store data and dispatches background jobs
27+
* to process the selected stores and add them to the discount system.
28+
*
29+
*/
30+
class CommunityStoresForm
31+
{
32+
public static function configure()
33+
{
34+
// Authorization check: prevent admin users from accessing this form
35+
// (admins likely have different workflows or permissions)
36+
if (Auth::user()?->role == 'admin') {
37+
Notification::make()
38+
->title('You are not authorized to access this page')
39+
->danger()
40+
->send();
41+
}
42+
43+
return Action::make('add_community_stores')
44+
->icon(Heroicon::BuildingStorefront)
45+
->schema([
46+
Wizard::make([
47+
48+
// Step 1: Store Search and Selection
49+
// Allows users to search for stores by name or browse alphabetically
50+
Step::make('Search Stores')
51+
->schema([
52+
53+
// Search input with 600ms debounce to prevent excessive API calls
54+
// Updates results dynamically as user types
55+
TextInput::make('search')
56+
->live(debounce: 600)
57+
->label('Search for a store'),
58+
59+
// Generates A-Z buttons dynamically for quick filtering
60+
ToggleButtons::make('letter')
61+
->label('Choose a letter')
62+
->live()
63+
->options(array_combine(range('A', 'Z'), range('A', 'Z')))
64+
->default(-1)
65+
->inline()
66+
->required()
67+
->reactive(),
68+
69+
// Visual store selection using image checkboxes
70+
// Dynamically loads stores based on search/letter filter and displays them with logos
71+
// Responsive grid layout adjusts from 1 column on mobile to 4 columns on large screens
72+
ImageCheckboxGroup::make('custom_stores')
73+
->options(function ($get, $set) {
74+
$stores = StoreHelper::search_community_stores($get('search'), $get('letter'));
75+
76+
$final_files = [];
77+
78+
foreach ($stores as $store)
79+
$final_files[$store['path']] = [
80+
'label' => $store['name'],
81+
'image' => $store['image'],
82+
'path' => $store['path'],
83+
];
84+
85+
return $final_files;
86+
})
87+
->gridColumns([
88+
'default' => 1,
89+
'sm' => 2,
90+
'md' => 3,
91+
'lg' => 4,
92+
'xl' => 4,
93+
'2xl' => 4,
94+
])
95+
->live()
96+
->minSelect(1),
97+
98+
// Hidden field to trigger domain fetch in the next step
99+
// This prevents premature API calls before user confirms store selection
100+
Hidden::make('domains_fetched')
101+
->default(false),
102+
103+
])
104+
->afterValidation(function ($state, $set) {
105+
// This runs when clicking "Next" after validation passes
106+
// Marks that user has completed store selection and triggers domain fetching in next step
107+
$set('domains_fetched', true);
108+
}),
109+
110+
// Step 2: Domain Selection
111+
// Some stores have multiple domains/websites; this step lets users choose specific ones
112+
Step::make('Select Domains')
113+
->key('select-domains')
114+
->schema([
115+
Hidden::make('domains_selected_files'),
116+
117+
// Domain selection with visual checkboxes
118+
// Fetches available domains for the stores selected in previous step
119+
ImageCheckboxGroup::make('domains_selected')
120+
->options(function ($get, $set) {
121+
122+
// Only fetch domains after user completes first step (prevents unnecessary API calls)
123+
// Returns empty array if user navigates back or hasn't completed step 1
124+
if (! $get('domains_fetched')) {
125+
return [];
126+
}
127+
128+
$customStores = $get('custom_stores');
129+
130+
$stores = StoreHelper::get_domains_for_stores($customStores);
131+
132+
$set('domains_selected_files', $stores);
133+
134+
return $stores;
135+
})
136+
->gridColumns([
137+
'default' => 1,
138+
'sm' => 2,
139+
'md' => 3,
140+
'lg' => 4,
141+
'xl' => 4,
142+
'2xl' => 4,
143+
])
144+
->live()
145+
->minSelect(1),
146+
147+
]),
148+
]),
149+
])
150+
->action(function ($data) {
151+
// Process selected domains and dispatch background jobs
152+
// Each job adds a store to the discount system with its logo from GitHub
153+
foreach ($data['domains_selected_files'] as $domain) {
154+
$image_link = config('settings.github_community_store_gist_base').$domain['store'].'/logo.png';
155+
AddStoreToDiscountJob::dispatch($domain['path'], $image_link);
156+
}
157+
});
158+
}
159+
}

app/Filament/Pages/Dashboard.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Filament\Pages;
4+
5+
use App\Filament\Widgets\ProductsStats;
6+
use App\Filament\Widgets\TotalStatsOverview;
7+
use Filament\Schemas\Contracts\HasSchemas;
8+
9+
class Dashboard extends \Filament\Pages\Dashboard implements HasSchemas
10+
{
11+
12+
public function getWidgets(): array
13+
{
14+
return [
15+
ProductsStats::class,
16+
];
17+
}
18+
19+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\Currencies;
4+
5+
use App\Filament\Resources\Currencies\Pages\CreateCurrency;
6+
use App\Filament\Resources\Currencies\Pages\EditCurrency;
7+
use App\Filament\Resources\Currencies\Pages\ListCurrencies;
8+
use App\Filament\Resources\Currencies\Schemas\CurrencyForm;
9+
use App\Filament\Resources\Currencies\Tables\CurrenciesTable;
10+
use App\Models\Currency;
11+
use BackedEnum;
12+
use Filament\Resources\Resource;
13+
use Filament\Schemas\Schema;
14+
use Filament\Support\Icons\Heroicon;
15+
use Filament\Tables\Table;
16+
use UnitEnum;
17+
18+
class CurrencyResource extends Resource
19+
{
20+
protected static ?string $model = Currency::class;
21+
22+
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
23+
24+
protected static ?string $recordTitleAttribute = 'name';
25+
26+
protected static string | UnitEnum | null $navigationGroup = 'Settings';
27+
28+
public static function form(Schema $schema): Schema
29+
{
30+
return CurrencyForm::configure($schema);
31+
}
32+
33+
public static function table(Table $table): Table
34+
{
35+
return CurrenciesTable::configure($table);
36+
}
37+
38+
public static function getRelations(): array
39+
{
40+
return [
41+
//
42+
];
43+
}
44+
45+
public static function getPages(): array
46+
{
47+
return [
48+
'index' => ListCurrencies::route('/'),
49+
'create' => CreateCurrency::route('/create'),
50+
'edit' => EditCurrency::route('/{record}/edit'),
51+
];
52+
}
53+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\Currencies\Pages;
4+
5+
use App\Filament\Resources\Currencies\CurrencyResource;
6+
use Filament\Resources\Pages\CreateRecord;
7+
8+
class CreateCurrency extends CreateRecord
9+
{
10+
protected static string $resource = CurrencyResource::class;
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\Currencies\Pages;
4+
5+
use App\Filament\Resources\Currencies\CurrencyResource;
6+
use Filament\Actions\DeleteAction;
7+
use Filament\Resources\Pages\EditRecord;
8+
9+
class EditCurrency extends EditRecord
10+
{
11+
protected static string $resource = CurrencyResource::class;
12+
13+
protected function getHeaderActions(): array
14+
{
15+
return [
16+
DeleteAction::make(),
17+
];
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\Currencies\Pages;
4+
5+
use App\Filament\Resources\Currencies\CurrencyResource;
6+
use Filament\Actions\CreateAction;
7+
use Filament\Resources\Pages\ListRecords;
8+
9+
class ListCurrencies extends ListRecords
10+
{
11+
protected static string $resource = CurrencyResource::class;
12+
13+
protected function getHeaderActions(): array
14+
{
15+
return [
16+
CreateAction::make(),
17+
];
18+
}
19+
}

0 commit comments

Comments
 (0)