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 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/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/resources/views/card-grid.blade.php b/resources/views/card-grid.blade.php new file mode 100644 index 0000000..a4b53bb --- /dev/null +++ b/resources/views/card-grid.blade.php @@ -0,0 +1,25 @@ +@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 h-full" + @else + class="h-full" + @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..ae29214 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,20 @@
@if($this->rows->count()) + + @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
@@ -210,6 +228,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..70c5383 --- /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;