From 50853728bcb785244bdf1748647e63c66141e6d4 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 30 Dec 2025 09:43:05 +0100 Subject: [PATCH] refactor(#120): remove unnecessary js, improve cache handling --- .../views/livewire/catalog/index.blade.php | 150 +++------ .../views/livewire/catalog/trmnl.blade.php | 303 ++++++++---------- tests/Feature/Livewire/Catalog/IndexTest.php | 83 +++++ tests/Feature/Volt/CatalogTrmnlTest.php | 62 ++++ 4 files changed, 324 insertions(+), 274 deletions(-) diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php index 83a34fc..3a24b7e 100644 --- a/resources/views/livewire/catalog/index.blade.php +++ b/resources/views/livewire/catalog/index.blade.php @@ -1,20 +1,24 @@ filter(function ($plugin) use ($currentVersion) { // Check if Laravel compatibility is true - if (!Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) { + if (! Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) { return false; } @@ -81,8 +85,9 @@ class extends Component { }) ->sortBy('name') ->toArray(); - } catch (\Exception $e) { - Log::error('Failed to load catalog from URL: ' . $e->getMessage()); + } catch (Exception $e) { + Log::error('Failed to load catalog from URL: '.$e->getMessage()); + return []; } }); @@ -94,8 +99,9 @@ class extends Component { $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId); - if (!$plugin || !$plugin['zip_url']) { + if (! $plugin || ! $plugin['zip_url']) { $this->addError('installation', 'Plugin not found or no download URL available.'); + return; } @@ -113,8 +119,8 @@ class extends Component { $this->dispatch('plugin-installed'); Flux::modal('import-from-catalog')->close(); - } catch (\Exception $e) { - $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); + } catch (Exception $e) { + $this->addError('installation', 'Error installing plugin: '.$e->getMessage()); } finally { $this->installingPlugin = ''; } @@ -124,32 +130,27 @@ class extends Component { { $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId); - if (!$plugin) { + if (! $plugin) { $this->addError('preview', 'Plugin not found.'); + return; } $this->previewingPlugin = $pluginId; $this->previewData = $plugin; - - // Store scroll position for restoration later - $this->dispatch('store-scroll-position'); } public function closePreview(): void { $this->previewingPlugin = ''; $this->previewData = []; - - // Restore scroll position when returning to catalog - $this->dispatch('restore-scroll-position'); } }; ?>
@if(empty($catalogPlugins))
- + No plugins available Catalog is empty
@@ -165,25 +166,25 @@ class extends Component { @if($plugin['logo_url']) {{ $plugin['name'] }} @else -
- +
+
@endif
-

{{ $plugin['name'] }}

+ {{ $plugin['name'] }} @if ($plugin['github']) -

by {{ $plugin['github'] }}

+ by {{ $plugin['github'] }} @endif
@if($plugin['license']) - {{ $plugin['license'] }} + {{ $plugin['license'] }} @endif @if($plugin['repo_url']) - + @endif @@ -191,7 +192,7 @@ class extends Component {
@if($plugin['description']) -

{{ $plugin['description'] }}

+ {{ $plugin['description'] }} @endif
@@ -201,14 +202,16 @@ class extends Component { Install - - - Preview - - + @if($plugin['screenshot_url']) + + + Preview + + + @endif @@ -236,34 +239,20 @@ class extends Component {
- @if($previewData['screenshot_url']) -
- Preview of {{ $previewData['name'] }} -
- @elseif($previewData['logo_url']) -
- {{ $previewData['name'] }} logo -

No preview image available

-
- @else -
- -

No preview available

-
- @endif +
+ Preview of {{ $previewData['name'] }} +
@if($previewData['description']) -
-

Description

-

{{ $previewData['description'] }}

+
+ Description + {{ $previewData['description'] }}
@endif -
+
- -@script - -@endscript diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index 1b5dd50..dd97e0e 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -1,20 +1,24 @@ 'newest', ]); - if (!$response->successful()) { - throw new \RuntimeException('Failed to fetch TRMNL recipes'); + if (! $response->successful()) { + throw new RuntimeException('Failed to fetch TRMNL recipes'); } $json = $response->json(); $data = $json['data'] ?? []; + return $this->mapRecipes($data); }); - } catch (\Throwable $e) { - Log::error('TRMNL catalog load error: ' . $e->getMessage()); + } catch (Throwable $e) { + Log::error('TRMNL catalog load error: '.$e->getMessage()); $this->recipes = []; } } @@ -62,23 +67,24 @@ class extends Component { { $this->isSearching = true; try { - $cacheKey = 'trmnl_recipes_search_' . md5($term); + $cacheKey = 'trmnl_recipes_search_'.md5($term); $this->recipes = Cache::remember($cacheKey, 300, function () use ($term) { $response = Http::get('https://usetrmnl.com/recipes.json', [ 'search' => $term, 'sort-by' => 'newest', ]); - if (!$response->successful()) { - throw new \RuntimeException('Failed to search TRMNL recipes'); + if (! $response->successful()) { + throw new RuntimeException('Failed to search TRMNL recipes'); } $json = $response->json(); $data = $json['data'] ?? []; + return $this->mapRecipes($data); }); - } catch (\Throwable $e) { - Log::error('TRMNL catalog search error: ' . $e->getMessage()); + } catch (Throwable $e) { + Log::error('TRMNL catalog search error: '.$e->getMessage()); $this->recipes = []; } finally { $this->isSearching = false; @@ -87,13 +93,14 @@ class extends Component { public function updatedSearch(): void { - $term = trim($this->search); + $term = mb_trim($this->search); if ($term === '') { $this->loadNewest(); + return; } - if (strlen($term) < 2) { + if (mb_strlen($term) < 2) { // Require at least 2 chars to avoid noisy calls return; } @@ -121,62 +128,78 @@ class extends Component { $this->dispatch('plugin-installed'); Flux::modal('import-from-trmnl-catalog')->close(); - } catch (\Exception $e) { - Log::error('Plugin installation failed: ' . $e->getMessage()); - $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); + } catch (Exception $e) { + Log::error('Plugin installation failed: '.$e->getMessage()); + $this->addError('installation', 'Error installing plugin: '.$e->getMessage()); } } public function previewRecipe(string $recipeId): void { - $recipe = collect($this->recipes)->firstWhere('id', $recipeId); - - if (!$recipe) { - $this->addError('preview', 'Recipe not found.'); - return; - } - $this->previewingRecipe = $recipeId; - $this->previewData = $recipe; - - // Store scroll position for restoration later - $this->dispatch('store-scroll-position'); - } - - public function closePreview(): void - { - $this->previewingRecipe = ''; $this->previewData = []; - // Restore scroll position when returning to catalog - $this->dispatch('restore-scroll-position'); + try { + $response = Http::timeout(10)->get("https://usetrmnl.com/recipes/{$recipeId}.json"); + + if ($response->successful()) { + $item = $response->json()['data'] ?? []; + $this->previewData = $this->mapRecipe($item); + } else { + // Fallback to searching for the specific recipe if single endpoint doesn't exist + $response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [ + 'search' => $recipeId, + ]); + + if ($response->successful()) { + $data = $response->json()['data'] ?? []; + $item = collect($data)->firstWhere('id', $recipeId); + if ($item) { + $this->previewData = $this->mapRecipe($item); + } + } + } + } catch (Throwable $e) { + Log::error('TRMNL catalog preview fetch error: '.$e->getMessage()); + } + + if (empty($this->previewData)) { + $this->previewData = collect($this->recipes)->firstWhere('id', $recipeId) ?? []; + } } /** - * @param array> $items + * @param array> $items * @return array> */ private function mapRecipes(array $items): array { return collect($items) - ->map(function (array $item) { - return [ - 'id' => $item['id'] ?? null, - 'name' => $item['name'] ?? 'Untitled', - 'icon_url' => $item['icon_url'] ?? null, - 'screenshot_url' => $item['screenshot_url'] ?? null, - 'author_bio' => is_array($item['author_bio'] ?? null) - ? strip_tags($item['author_bio']['description'] ?? null) - : null, - 'stats' => [ - 'installs' => data_get($item, 'stats.installs'), - 'forks' => data_get($item, 'stats.forks'), - ], - 'detail_url' => isset($item['id']) ? ('https://usetrmnl.com/recipes/' . $item['id']) : null, - ]; - }) + ->map(fn (array $item) => $this->mapRecipe($item)) ->toArray(); } + + /** + * @param array $item + * @return array + */ + private function mapRecipe(array $item): array + { + return [ + 'id' => $item['id'] ?? null, + 'name' => $item['name'] ?? 'Untitled', + 'icon_url' => $item['icon_url'] ?? null, + 'screenshot_url' => $item['screenshot_url'] ?? null, + 'author_bio' => is_array($item['author_bio'] ?? null) + ? strip_tags($item['author_bio']['description'] ?? null) + : null, + 'stats' => [ + 'installs' => data_get($item, 'stats.installs'), + 'forks' => data_get($item, 'stats.forks'), + ], + 'detail_url' => isset($item['id']) ? ('https://usetrmnl.com/recipes/'.$item['id']) : null, + ]; + } }; ?>
@@ -188,7 +211,7 @@ class extends Component { icon="magnifying-glass" />
- Newest + Newest
@error('installation') @@ -197,7 +220,7 @@ class extends Component { @if(empty($recipes))
- + No recipes found Try a different search term
@@ -211,8 +234,8 @@ class extends Component { @if($thumb) {{ $recipe['name'] }} @else -
- +
+
@endif @@ -221,12 +244,12 @@ class extends Component {
{{ $recipe['name'] }} @if(data_get($recipe, 'stats.installs')) - Installs: {{ data_get($recipe, 'stats.installs') }} · Forks: {{ data_get($recipe, 'stats.forks') }} + Installs: {{ data_get($recipe, 'stats.installs') }} · Forks: {{ data_get($recipe, 'stats.forks') }} @endif
@if($recipe['detail_url']) - + @endif @@ -246,7 +269,7 @@ class extends Component { @endif - @if($recipe['id']) + @if($recipe['id'] && ($recipe['screenshot_url'] ?? null)) - @if($previewingRecipe && !empty($previewData)) -
- Preview {{ $previewData['name'] ?? 'Recipe' }} +
+
+ + Fetching recipe details...
+
-
- @if($previewData['screenshot_url']) +
+ @if($previewingRecipe && !empty($previewData)) +
+ Preview {{ $previewData['name'] ?? 'Recipe' }} +
+ +
Preview of {{ $previewData['name'] }}
- @elseif($previewData['icon_url']) -
- {{ $previewData['name'] }} icon - No preview image available -
- @else -
- - No preview available -
- @endif - @if($previewData['author_bio']) -
-
- Description - {{ $previewData['author_bio'] }} + @if($previewData['author_bio']) +
+
+ Description + {{ $previewData['author_bio'] }} +
-
- @endif - - @if(data_get($previewData, 'stats.installs')) -
-
- Statistics - - Installs: {{ data_get($previewData, 'stats.installs') }} · - Forks: {{ data_get($previewData, 'stats.forks') }} - -
-
- @endif - -
- @if($previewData['detail_url']) - - View on TRMNL - @endif - - - Install Recipe - - + + @if(data_get($previewData, 'stats.installs')) +
+
+ Statistics + + Installs: {{ data_get($previewData, 'stats.installs') }} · + Forks: {{ data_get($previewData, 'stats.forks') }} + +
+
+ @endif + +
+ @if($previewData['detail_url']) + + View on TRMNL + + @endif + + + Install Recipe + + +
-
- @endif + @endif +
- -@script - -@endscript diff --git a/tests/Feature/Livewire/Catalog/IndexTest.php b/tests/Feature/Livewire/Catalog/IndexTest.php index 22ab4b6..1b2efba 100644 --- a/tests/Feature/Livewire/Catalog/IndexTest.php +++ b/tests/Feature/Livewire/Catalog/IndexTest.php @@ -65,6 +65,46 @@ it('loads plugins from catalog URL', function (): void { $component->assertSee('testuser'); $component->assertSee('A test plugin'); $component->assertSee('MIT'); + $component->assertSee('Preview'); +}); + +it('hides preview button when screenshot_url is missing', function (): void { + // Clear cache first to ensure fresh data + Cache::forget('catalog_plugins'); + + // Mock the HTTP response for the catalog URL without screenshot_url + $catalogData = [ + 'test-plugin' => [ + 'name' => 'Test Plugin Without Screenshot', + 'author' => ['name' => 'Test Author', 'github' => 'testuser'], + 'author_bio' => [ + 'description' => 'A test plugin', + ], + 'license' => 'MIT', + 'trmnlp' => [ + 'zip_url' => 'https://example.com/plugin.zip', + ], + 'byos' => [ + 'byos_laravel' => [ + 'compatibility' => true, + ], + ], + 'logo_url' => 'https://example.com/logo.png', + 'screenshot_url' => null, + ], + ]; + + $yamlContent = Yaml::dump($catalogData); + + Http::fake([ + config('app.catalog_url') => Http::response($yamlContent, 200), + ]); + + Livewire::withoutLazyLoading(); + + Volt::test('catalog.index') + ->assertSee('Test Plugin Without Screenshot') + ->assertDontSeeHtml('variant="subtle" icon="eye"'); }); it('shows error when plugin not found', function (): void { @@ -114,3 +154,46 @@ it('shows error when zip_url is missing', function (): void { $component->assertHasErrors(); }); + +it('can preview a plugin', function (): void { + // Clear cache first to ensure fresh data + Cache::forget('catalog_plugins'); + + // Mock the HTTP response for the catalog URL + $catalogData = [ + 'test-plugin' => [ + 'name' => 'Test Plugin', + 'author' => ['name' => 'Test Author', 'github' => 'testuser'], + 'author_bio' => [ + 'description' => 'A test plugin description', + ], + 'license' => 'MIT', + 'trmnlp' => [ + 'zip_url' => 'https://example.com/plugin.zip', + ], + 'byos' => [ + 'byos_laravel' => [ + 'compatibility' => true, + ], + ], + 'logo_url' => 'https://example.com/logo.png', + 'screenshot_url' => 'https://example.com/screenshot.png', + ], + ]; + + $yamlContent = Yaml::dump($catalogData); + + Http::fake([ + config('app.catalog_url') => Http::response($yamlContent, 200), + ]); + + Livewire::withoutLazyLoading(); + + Volt::test('catalog.index') + ->assertSee('Test Plugin') + ->call('previewPlugin', 'test-plugin') + ->assertSet('previewingPlugin', 'test-plugin') + ->assertSet('previewData.name', 'Test Plugin') + ->assertSee('Preview Test Plugin') + ->assertSee('A test plugin description'); +}); diff --git a/tests/Feature/Volt/CatalogTrmnlTest.php b/tests/Feature/Volt/CatalogTrmnlTest.php index ba1b722..4c338df 100644 --- a/tests/Feature/Volt/CatalogTrmnlTest.php +++ b/tests/Feature/Volt/CatalogTrmnlTest.php @@ -28,9 +28,33 @@ it('loads newest TRMNL recipes on mount', function (): void { Volt::test('catalog.trmnl') ->assertSee('Weather Chum') ->assertSee('Install') + ->assertDontSeeHtml('variant="subtle" icon="eye"') ->assertSee('Installs: 10'); }); +it('shows preview button when screenshot_url is provided', function (): void { + Http::fake([ + 'usetrmnl.com/recipes.json*' => Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => 'https://example.com/screenshot.png', + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + ]); + + Livewire::withoutLazyLoading(); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->assertSee('Preview'); +}); + it('searches TRMNL recipes when search term is provided', function (): void { Http::fake([ // First call (mount -> newest) @@ -152,3 +176,41 @@ it('shows error when plugin installation fails', function (): void { ->call('installPlugin', '123') ->assertSee('Error installing plugin'); // This will fail because the zip content is invalid }); + +it('previews a recipe with async fetch', function (): void { + Http::fake([ + 'usetrmnl.com/recipes.json*' => Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => 'https://example.com/old.png', + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + 'usetrmnl.com/recipes/123.json' => Http::response([ + 'data' => [ + 'id' => 123, + 'name' => 'Weather Chum Updated', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => 'https://example.com/new.png', + 'author_bio' => ['description' => 'New bio'], + 'stats' => ['installs' => 11, 'forks' => 3], + ], + ], 200), + ]); + + Livewire::withoutLazyLoading(); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->call('previewRecipe', '123') + ->assertSet('previewingRecipe', '123') + ->assertSet('previewData.name', 'Weather Chum Updated') + ->assertSet('previewData.screenshot_url', 'https://example.com/new.png') + ->assertSee('Preview Weather Chum Updated') + ->assertSee('New bio'); +});