loadNewest(); } public function placeholder() { return <<<'HTML'
Loading recipes...
HTML; } private function loadNewest(): void { try { $cacheKey = 'trmnl_recipes_newest_page_'.$this->page; $response = Cache::remember($cacheKey, 43200, function () { $response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [ 'sort-by' => 'newest', 'page' => $this->page, ]); if (! $response->successful()) { throw new RuntimeException('Failed to fetch TRMNL recipes'); } return $response->json(); }); $data = $response['data'] ?? []; $mapped = $this->mapRecipes($data); if ($this->page === 1) { $this->recipes = $mapped; } else { $this->recipes = array_merge($this->recipes, $mapped); } $this->hasMore = ! empty($response['next_page_url']); } catch (Throwable $e) { Log::error('TRMNL catalog load error: '.$e->getMessage()); if ($this->page === 1) { $this->recipes = []; } $this->hasMore = false; } } private function searchRecipes(string $term): void { $this->isSearching = true; try { $cacheKey = 'trmnl_recipes_search_'.md5($term).'_page_'.$this->page; $response = Cache::remember($cacheKey, 300, function () use ($term) { $response = Http::get('https://usetrmnl.com/recipes.json', [ 'search' => $term, 'sort-by' => 'newest', 'page' => $this->page, ]); if (! $response->successful()) { throw new RuntimeException('Failed to search TRMNL recipes'); } return $response->json(); }); $data = $response['data'] ?? []; $mapped = $this->mapRecipes($data); if ($this->page === 1) { $this->recipes = $mapped; } else { $this->recipes = array_merge($this->recipes, $mapped); } $this->hasMore = ! empty($response['next_page_url']); } catch (Throwable $e) { Log::error('TRMNL catalog search error: '.$e->getMessage()); if ($this->page === 1) { $this->recipes = []; } $this->hasMore = false; } finally { $this->isSearching = false; } } public function loadMore(): void { $this->page++; $term = mb_trim($this->search); if ($term === '' || mb_strlen($term) < 2) { $this->loadNewest(); } else { $this->searchRecipes($term); } } public function updatedSearch(): void { $this->page = 1; $term = mb_trim($this->search); if ($term === '') { $this->loadNewest(); return; } if (mb_strlen($term) < 2) { // Require at least 2 chars to avoid noisy calls return; } $this->searchRecipes($term); } public function installPlugin(string $recipeId, PluginImportService $pluginImportService): void { abort_unless(auth()->user() !== null, 403); try { $zipUrl = "https://usetrmnl.com/api/plugin_settings/{$recipeId}/archive"; $recipe = collect($this->recipes)->firstWhere('id', $recipeId); $plugin = $pluginImportService->importFromUrl( $zipUrl, auth()->user(), null, config('services.trmnl.liquid_enabled') ? 'trmnl-liquid' : null, $recipe['icon_url'] ?? null ); $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()); } } public function previewRecipe(string $recipeId): void { $this->previewingRecipe = $recipeId; $this->previewData = []; 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 * @return array> */ private function mapRecipes(array $items): array { return collect($items) ->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, ]; } }; ?>
Newest
@error('installation') @enderror @if(empty($recipes))
No recipes found Try a different search term
@else
@foreach($recipes as $recipe)
@php($thumb = $recipe['icon_url'] ?? $recipe['screenshot_url']) @if($thumb) {{ $recipe['name'] }} @else
@endif
{{ $recipe['name'] }} @if(data_get($recipe, 'stats.installs')) Installs: {{ data_get($recipe, 'stats.installs') }} · Forks: {{ data_get($recipe, 'stats.forks') }} @endif
@if($recipe['detail_url']) @endif
@if($recipe['author_bio']) {{ $recipe['author_bio'] }} @endif
@if($recipe['id']) Install @endif @if($recipe['id'] && ($recipe['screenshot_url'] ?? null)) Preview @endif
@endforeach
@if($hasMore)
Load next page Loading...
@endif @endif
Fetching recipe details...
@if($previewingRecipe && !empty($previewData))
Preview {{ $previewData['name'] ?? 'Recipe' }}
Preview of {{ $previewData['name'] }}
@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
@endif