From 8a63d94f669422ddefa0e30b794b51b819738c70 Mon Sep 17 00:00:00 2001 From: Simon Barrett Date: Fri, 6 Mar 2026 09:06:34 +0000 Subject: [PATCH 1/4] Add card view mode as alternative to table rows Introduces an opt-in card grid view that can be toggled from the toolbar. Existing apps are unaffected unless they override cardLayout(). Closes #103 Co-Authored-By: Claude Opus 4.6 --- resources/views/card-grid.blade.php | 23 ++++++ resources/views/card.blade.php | 15 ++++ .../views/components/view-toggle.blade.php | 30 ++++++++ resources/views/query-table.blade.php | 8 ++ resources/views/table.blade.php | 9 +++ src/QueryBuilder.php | 2 + src/Support/CardLayout.php | 76 +++++++++++++++++++ src/Support/Concerns/WithCardView.php | 30 ++++++++ src/TableBuilder.php | 2 + 9 files changed, 195 insertions(+) create mode 100644 resources/views/card-grid.blade.php create mode 100644 resources/views/card.blade.php create mode 100644 resources/views/components/view-toggle.blade.php create mode 100644 src/Support/CardLayout.php create mode 100644 src/Support/Concerns/WithCardView.php diff --git a/resources/views/card-grid.blade.php b/resources/views/card-grid.blade.php new file mode 100644 index 0000000..38696dd --- /dev/null +++ b/resources/views/card-grid.blade.php @@ -0,0 +1,23 @@ +@php + $cardLayout = $this->cardLayout(); + $gridCols = $cardLayout->getGridColumns(); +@endphp + +
+ @foreach($this->rows as $row) + @php + $imageUrl = $cardLayout->getImageUrl($row); + $cardView = $cardLayout->getCardView(); + @endphp + +
isClickable()) + {!! $this->renderRowClick($row->id) !!} + class="cursor-pointer" + @endif + > + @include($cardView, ['row' => $row, 'imageUrl' => $imageUrl]) +
+ @endforeach +
diff --git a/resources/views/card.blade.php b/resources/views/card.blade.php new file mode 100644 index 0000000..d517629 --- /dev/null +++ b/resources/views/card.blade.php @@ -0,0 +1,15 @@ +
+ @if($imageUrl) +
+ +
+ @endif + +
+

{{ $row->id }}

+
+
diff --git a/resources/views/components/view-toggle.blade.php b/resources/views/components/view-toggle.blade.php new file mode 100644 index 0000000..9705932 --- /dev/null +++ b/resources/views/components/view-toggle.blade.php @@ -0,0 +1,30 @@ +
+ + +
diff --git a/resources/views/query-table.blade.php b/resources/views/query-table.blade.php index 8204e86..ce48788 100644 --- a/resources/views/query-table.blade.php +++ b/resources/views/query-table.blade.php @@ -25,6 +25,10 @@
+ @if($this->isCardViewEnabled()) + @include('query-builder::components.view-toggle') + @endif + @if($this->isSearchVisible()) @include('query-builder::components.search') @endif @@ -49,6 +53,9 @@ @if($this->rows->count()) + @if($this->isCardMode()) + @include('query-builder::card-grid') + @else
@@ -173,6 +180,7 @@ class="sr-only ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">
+ @endif @if($this->isPaginated() && $this->rows->hasPages())
diff --git a/resources/views/table.blade.php b/resources/views/table.blade.php index a41ab3b..16fae4c 100644 --- a/resources/views/table.blade.php +++ b/resources/views/table.blade.php @@ -8,6 +8,10 @@ @if($this->isSearchVisible() && $this->searchableColumnsSet() && ! $this->areActionsVisible()) @if($this->isFiltered() || $this->isSearchActive() || $this->rows->count() > 0)
+ @if($this->isCardViewEnabled()) + @include('query-builder::components.view-toggle') + @endif + @include('query-builder::components.search')
@endif @@ -43,6 +47,10 @@
@if($this->rows->count()) + + @if($this->isCardMode()) + @include('query-builder::card-grid') + @else
@@ -210,6 +218,7 @@ class="flex justify-center items-center absolute inset-0" @endif + @endif @else
diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 0156ca2..b2b2a5f 100755 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -9,6 +9,7 @@ use ACTTraining\QueryBuilder\Support\Collection\CriteriaCollection; use ACTTraining\QueryBuilder\Support\Concerns\WithActions; use ACTTraining\QueryBuilder\Support\Concerns\WithCaching; +use ACTTraining\QueryBuilder\Support\Concerns\WithCardView; use ACTTraining\QueryBuilder\Support\Concerns\WithColumns; use ACTTraining\QueryBuilder\Support\Concerns\WithFilters; use ACTTraining\QueryBuilder\Support\Concerns\WithIdentifier; @@ -36,6 +37,7 @@ abstract class QueryBuilder extends Component { use WithActions; use WithCaching; + use WithCardView; use WithColumns; use WithFilters; use WithIdentifier; diff --git a/src/Support/CardLayout.php b/src/Support/CardLayout.php new file mode 100644 index 0000000..a8d2984 --- /dev/null +++ b/src/Support/CardLayout.php @@ -0,0 +1,76 @@ +imageKey = $key; + + return $this; + } + + public function placeholder(string $url): static + { + $this->placeholderImage = $url; + + return $this; + } + + public function columns(int $columns): static + { + $this->gridColumns = $columns; + + return $this; + } + + public function view(string $view): static + { + $this->cardView = $view; + + return $this; + } + + public function getImageKey(): ?string + { + return $this->imageKey; + } + + public function getPlaceholderImage(): ?string + { + return $this->placeholderImage; + } + + public function getGridColumns(): int + { + return $this->gridColumns; + } + + public function getCardView(): string + { + return $this->cardView; + } + + public function getImageUrl(mixed $row): ?string + { + if (! $this->imageKey) { + return null; + } + + return data_get($row, $this->imageKey) ?? $this->placeholderImage; + } +} diff --git a/src/Support/Concerns/WithCardView.php b/src/Support/Concerns/WithCardView.php new file mode 100644 index 0000000..bc79ea8 --- /dev/null +++ b/src/Support/Concerns/WithCardView.php @@ -0,0 +1,30 @@ +viewMode = $this->viewMode === 'table' ? 'cards' : 'table'; + } + + public function isCardViewEnabled(): bool + { + return $this->cardLayout() !== null; + } + + public function isCardMode(): bool + { + return $this->isCardViewEnabled() && $this->viewMode === 'cards'; + } +} diff --git a/src/TableBuilder.php b/src/TableBuilder.php index 6b5cf8b..30e228f 100755 --- a/src/TableBuilder.php +++ b/src/TableBuilder.php @@ -5,6 +5,7 @@ namespace ACTTraining\QueryBuilder; use ACTTraining\QueryBuilder\Support\Concerns\WithActions; +use ACTTraining\QueryBuilder\Support\Concerns\WithCardView; use ACTTraining\QueryBuilder\Support\Concerns\WithColumns; use ACTTraining\QueryBuilder\Support\Concerns\WithFilters; use ACTTraining\QueryBuilder\Support\Concerns\WithIdentifier; @@ -31,6 +32,7 @@ abstract class TableBuilder extends Component { use WithActions; + use WithCardView; use WithColumns; use WithFilters; use WithIdentifier; From e0df15937e1e5ffbfd22518adaa2e8a9bcf40c4f Mon Sep 17 00:00:00 2001 From: Simon Barrett Date: Fri, 6 Mar 2026 09:08:29 +0000 Subject: [PATCH 2/4] Fix deprecated checkMissingIterableValueType PHPStan config Replace deprecated checkMissingIterableValueType option with the recommended ignoreErrors identifier approach. Co-Authored-By: Claude Opus 4.6 --- phpstan.neon.dist | 5 ++++- src/Support/CardLayout.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 489fa4e..72ab4c8 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -10,5 +10,8 @@ parameters: tmpDir: build/phpstan checkOctaneCompatibility: true checkModelProperties: true - checkMissingIterableValueType: false + ignoreErrors: + - + identifier: missingType.iterableValue + reportUnmatched: false diff --git a/src/Support/CardLayout.php b/src/Support/CardLayout.php index a8d2984..70c5383 100644 --- a/src/Support/CardLayout.php +++ b/src/Support/CardLayout.php @@ -12,7 +12,7 @@ class CardLayout protected string $cardView = 'query-builder::card'; - public static function make(): static + public static function make(): self { return new self; } From 2104ac808025f3e475fde5716648331846465c2f Mon Sep 17 00:00:00 2001 From: Simon Barrett Date: Fri, 6 Mar 2026 09:13:30 +0000 Subject: [PATCH 3/4] Add card view documentation Co-Authored-By: Claude Opus 4.6 --- CARD-VIEW.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 CARD-VIEW.md diff --git a/CARD-VIEW.md b/CARD-VIEW.md new file mode 100644 index 0000000..400c571 --- /dev/null +++ b/CARD-VIEW.md @@ -0,0 +1,104 @@ +# Card View + +An opt-in alternative to the default table row layout that displays results as a grid of cards. + +## Basic Setup + +Override `cardLayout()` in any `TableBuilder` or `QueryBuilder` component to enable the card view toggle: + +```php +use ACTTraining\QueryBuilder\Support\CardLayout; + +class CoursesTable extends TableBuilder +{ + public function cardLayout(): ?CardLayout + { + return CardLayout::make() + ->columns(3) + ->view('courses.card'); + } +} +``` + +This adds the grid/list toggle buttons to the toolbar. Without overriding `cardLayout()`, nothing changes — the toggle is hidden by default. + +## Card Blade View + +Create the Blade partial referenced in `->view()`. It receives `$row` (the Eloquent model) and `$imageUrl` (resolved image URL or null): + +```blade +{{-- resources/views/courses/card.blade.php --}} +
+ @if($imageUrl) +
+ +
+ @endif + +
+

{{ $row->category->name }}

+

{{ $row->title }}

+ +
+

{{ $row->duration }} hours

+

{{ $row->location }}

+

{{ $row->delivery_method }}

+
+
+
+``` + +## CardLayout Options + +| Method | Description | Default | +|--------|-------------|---------| +| `->columns(int)` | Number of grid columns at `lg` breakpoint | `3` | +| `->image(string)` | Dot-notation key for the image URL on the model (e.g. `'photo_url'` or `'media.url'`) | `null` (no image) | +| `->placeholder(string)` | Fallback image URL when the model's image is null | `null` | +| `->view(string)` | Blade view to render each card | `'query-builder::card'` (basic default) | + +## Examples + +### Cards with images and a placeholder + +```php +public function cardLayout(): ?CardLayout +{ + return CardLayout::make() + ->image('featured_image_url') + ->placeholder('/img/placeholder.jpg') + ->columns(3) + ->view('courses.card'); +} +``` + +### Cards without images (text-only) + +```php +public function cardLayout(): ?CardLayout +{ + return CardLayout::make() + ->columns(4) + ->view('contacts.card'); +} +``` + +### Using the default card template + +No custom view needed — renders image + model ID: + +```php +public function cardLayout(): ?CardLayout +{ + return CardLayout::make() + ->image('avatar_url'); +} +``` + +## What still works in card mode + +- **Pagination** — unchanged +- **Search** — unchanged +- **Filters / criteria** — unchanged +- **Sorting** — applied at the query level, so cards render in sorted order +- **Row click** — if `isClickable()` is true, clicking a card triggers the same action as clicking a table row From 1135b73874ebe166f133a025f48b16adb1c492a4 Mon Sep 17 00:00:00 2001 From: Simon Barrett Date: Fri, 6 Mar 2026 10:23:49 +0000 Subject: [PATCH 4/4] Add pagination, equal height cards, and Livewire 4 support to card view - Add pagination links below card grid when paginated - Add h-full class to card wrapper divs for equal height cards - Support Livewire ^4.0 in addition to ^3.2.6 Co-Authored-By: Claude Opus 4.6 --- composer.json | 2 +- resources/views/card-grid.blade.php | 4 +++- resources/views/table.blade.php | 10 ++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 0136497..1ba189d 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": "^8.2", - "livewire/livewire": "^v3.2.6", + "livewire/livewire": "^v3.2.6|^4.0", "secondnetwork/blade-tabler-icons": "^3.24", "spatie/laravel-package-tools": "^1.14.0" }, diff --git a/resources/views/card-grid.blade.php b/resources/views/card-grid.blade.php index 38696dd..a4b53bb 100644 --- a/resources/views/card-grid.blade.php +++ b/resources/views/card-grid.blade.php @@ -14,7 +14,9 @@ wire:key="{{ $this->identifier() }}-card-{{ $row->id }}" @if($this->isClickable()) {!! $this->renderRowClick($row->id) !!} - class="cursor-pointer" + class="cursor-pointer h-full" + @else + class="h-full" @endif > @include($cardView, ['row' => $row, 'imageUrl' => $imageUrl]) diff --git a/resources/views/table.blade.php b/resources/views/table.blade.php index 16fae4c..ae29214 100644 --- a/resources/views/table.blade.php +++ b/resources/views/table.blade.php @@ -50,6 +50,16 @@ @if($this->isCardMode()) @include('query-builder::card-grid') + + @if($this->isPaginated() && $this->rows->hasPages()) +
+ @if($this->scroll() === true) + {{ $this->rows->links() }} + @else + {{ $this->rows->links(data: ['scrollTo' => $this->scroll()]) }} + @endif +
+ @endif @else