mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
feat: add trmnl catalog paginator
This commit is contained in:
parent
3250bb0402
commit
7f97114f6e
2 changed files with 134 additions and 13 deletions
|
|
@ -13,6 +13,10 @@ class extends Component
|
||||||
{
|
{
|
||||||
public array $recipes = [];
|
public array $recipes = [];
|
||||||
|
|
||||||
|
public int $page = 1;
|
||||||
|
|
||||||
|
public bool $hasMore = false;
|
||||||
|
|
||||||
public string $search = '';
|
public string $search = '';
|
||||||
|
|
||||||
public bool $isSearching = false;
|
public bool $isSearching = false;
|
||||||
|
|
@ -43,23 +47,36 @@ class extends Component
|
||||||
private function loadNewest(): void
|
private function loadNewest(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->recipes = Cache::remember('trmnl_recipes_newest', 43200, function () {
|
$cacheKey = 'trmnl_recipes_newest_page_'.$this->page;
|
||||||
|
$response = Cache::remember($cacheKey, 43200, function () {
|
||||||
$response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [
|
$response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [
|
||||||
'sort-by' => 'newest',
|
'sort-by' => 'newest',
|
||||||
|
'page' => $this->page,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (! $response->successful()) {
|
if (! $response->successful()) {
|
||||||
throw new RuntimeException('Failed to fetch TRMNL recipes');
|
throw new RuntimeException('Failed to fetch TRMNL recipes');
|
||||||
}
|
}
|
||||||
|
|
||||||
$json = $response->json();
|
return $response->json();
|
||||||
$data = $json['data'] ?? [];
|
|
||||||
|
|
||||||
return $this->mapRecipes($data);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$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) {
|
} catch (Throwable $e) {
|
||||||
Log::error('TRMNL catalog load error: '.$e->getMessage());
|
Log::error('TRMNL catalog load error: '.$e->getMessage());
|
||||||
$this->recipes = [];
|
if ($this->page === 1) {
|
||||||
|
$this->recipes = [];
|
||||||
|
}
|
||||||
|
$this->hasMore = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,32 +84,57 @@ class extends Component
|
||||||
{
|
{
|
||||||
$this->isSearching = true;
|
$this->isSearching = true;
|
||||||
try {
|
try {
|
||||||
$cacheKey = 'trmnl_recipes_search_'.md5($term);
|
$cacheKey = 'trmnl_recipes_search_'.md5($term).'_page_'.$this->page;
|
||||||
$this->recipes = Cache::remember($cacheKey, 300, function () use ($term) {
|
$response = Cache::remember($cacheKey, 300, function () use ($term) {
|
||||||
$response = Http::get('https://usetrmnl.com/recipes.json', [
|
$response = Http::get('https://usetrmnl.com/recipes.json', [
|
||||||
'search' => $term,
|
'search' => $term,
|
||||||
'sort-by' => 'newest',
|
'sort-by' => 'newest',
|
||||||
|
'page' => $this->page,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (! $response->successful()) {
|
if (! $response->successful()) {
|
||||||
throw new RuntimeException('Failed to search TRMNL recipes');
|
throw new RuntimeException('Failed to search TRMNL recipes');
|
||||||
}
|
}
|
||||||
|
|
||||||
$json = $response->json();
|
return $response->json();
|
||||||
$data = $json['data'] ?? [];
|
|
||||||
|
|
||||||
return $this->mapRecipes($data);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$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) {
|
} catch (Throwable $e) {
|
||||||
Log::error('TRMNL catalog search error: '.$e->getMessage());
|
Log::error('TRMNL catalog search error: '.$e->getMessage());
|
||||||
$this->recipes = [];
|
if ($this->page === 1) {
|
||||||
|
$this->recipes = [];
|
||||||
|
}
|
||||||
|
$this->hasMore = false;
|
||||||
} finally {
|
} finally {
|
||||||
$this->isSearching = false;
|
$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
|
public function updatedSearch(): void
|
||||||
{
|
{
|
||||||
|
$this->page = 1;
|
||||||
$term = mb_trim($this->search);
|
$term = mb_trim($this->search);
|
||||||
if ($term === '') {
|
if ($term === '') {
|
||||||
$this->loadNewest();
|
$this->loadNewest();
|
||||||
|
|
@ -286,6 +328,15 @@ class extends Component
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if($hasMore)
|
||||||
|
<div class="flex justify-center mt-6">
|
||||||
|
<flux:button wire:click="loadMore" variant="subtle" wire:loading.attr="disabled">
|
||||||
|
<span wire:loading.remove wire:target="loadMore">Load next page</span>
|
||||||
|
<span wire:loading wire:target="loadMore">Loading...</span>
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- Preview Modal -->
|
<!-- Preview Modal -->
|
||||||
|
|
|
||||||
|
|
@ -214,3 +214,73 @@ it('previews a recipe with async fetch', function (): void {
|
||||||
->assertSee('Preview Weather Chum Updated')
|
->assertSee('Preview Weather Chum Updated')
|
||||||
->assertSee('New bio');
|
->assertSee('New bio');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('supports pagination and loading more recipes', function (): void {
|
||||||
|
Http::fake([
|
||||||
|
'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::response([
|
||||||
|
'data' => [
|
||||||
|
[
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Recipe Page 1',
|
||||||
|
'icon_url' => null,
|
||||||
|
'screenshot_url' => null,
|
||||||
|
'author_bio' => null,
|
||||||
|
'stats' => ['installs' => 1, 'forks' => 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'next_page_url' => '/recipes.json?page=2',
|
||||||
|
], 200),
|
||||||
|
'usetrmnl.com/recipes.json?sort-by=newest&page=2' => Http::response([
|
||||||
|
'data' => [
|
||||||
|
[
|
||||||
|
'id' => 2,
|
||||||
|
'name' => 'Recipe Page 2',
|
||||||
|
'icon_url' => null,
|
||||||
|
'screenshot_url' => null,
|
||||||
|
'author_bio' => null,
|
||||||
|
'stats' => ['installs' => 2, 'forks' => 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'next_page_url' => null,
|
||||||
|
], 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::withoutLazyLoading();
|
||||||
|
|
||||||
|
Volt::test('catalog.trmnl')
|
||||||
|
->assertSee('Recipe Page 1')
|
||||||
|
->assertDontSee('Recipe Page 2')
|
||||||
|
->assertSee('Load next page')
|
||||||
|
->call('loadMore')
|
||||||
|
->assertSee('Recipe Page 1')
|
||||||
|
->assertSee('Recipe Page 2')
|
||||||
|
->assertDontSee('Load next page');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets pagination when search term changes', function (): void {
|
||||||
|
Http::fake([
|
||||||
|
'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::sequence()
|
||||||
|
->push([
|
||||||
|
'data' => [['id' => 1, 'name' => 'Initial 1']],
|
||||||
|
'next_page_url' => '/recipes.json?page=2',
|
||||||
|
])
|
||||||
|
->push([
|
||||||
|
'data' => [['id' => 3, 'name' => 'Initial 1 Again']],
|
||||||
|
'next_page_url' => null,
|
||||||
|
]),
|
||||||
|
'usetrmnl.com/recipes.json?search=weather&sort-by=newest&page=1' => Http::response([
|
||||||
|
'data' => [['id' => 2, 'name' => 'Weather Result']],
|
||||||
|
'next_page_url' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::withoutLazyLoading();
|
||||||
|
|
||||||
|
Volt::test('catalog.trmnl')
|
||||||
|
->assertSee('Initial 1')
|
||||||
|
->call('loadMore')
|
||||||
|
->set('search', 'weather')
|
||||||
|
->assertSee('Weather Result')
|
||||||
|
->assertDontSee('Initial 1')
|
||||||
|
->assertSet('page', 1);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue