From 89abbf1f60c8b0ff6eadea7d9f71e749a3fa6105 Mon Sep 17 00:00:00 2001
From: Wessel Verheij
Date: Tue, 18 Aug 2026 20:50:38 +0200
Subject: [PATCH] Add cross-platform filter and per-platform screenshots to
showcase
Apps that support both mobile and desktop had no way to be filtered
for on the public showcase page - only "Mobile" or "Desktop" existed
as tabs, even though has_mobile/has_desktop were already independent
flags on the model. Adds a third "Cross-Platform" filter tab backed
by the existing withMobile()/withDesktop() scopes.
Also lets a submitter differentiate screenshots by platform once both
are selected, so the showcase card can show the Mobile set on the
Mobile filter and the Desktop set on the Desktop filter. This is
opt-in and additive: the existing screenshots field keeps working
exactly as before for single-platform apps, and is used as the
fallback for both-platform apps that never split their screenshots.
---
app/Filament/Resources/ShowcaseResource.php | 22 ++++
app/Http/Controllers/ShowcaseController.php | 2 +
app/Livewire/ShowcaseSubmissionForm.php | 67 +++++++++--
app/Models/Showcase.php | 18 +++
database/factories/ShowcaseFactory.php | 16 +++
...latform_screenshots_to_showcases_table.php | 29 +++++
database/seeders/ShowcaseSeeder.php | 1 +
.../views/components/showcase-card.blade.php | 14 ++-
.../showcase-submission-form.blade.php | 50 ++++++++
resources/views/showcase.blade.php | 20 +++-
routes/web.php | 2 +-
tests/Feature/ShowcasePageTest.php | 111 ++++++++++++++++++
tests/Feature/ShowcaseSubmissionTest.php | 67 +++++++++++
tests/Unit/ShowcaseTest.php | 68 +++++++++++
14 files changed, 468 insertions(+), 19 deletions(-)
create mode 100644 database/migrations/2026_08_18_120000_add_platform_screenshots_to_showcases_table.php
create mode 100644 tests/Feature/ShowcasePageTest.php
create mode 100644 tests/Unit/ShowcaseTest.php
diff --git a/app/Filament/Resources/ShowcaseResource.php b/app/Filament/Resources/ShowcaseResource.php
index f8416a1d..1c530cc9 100644
--- a/app/Filament/Resources/ShowcaseResource.php
+++ b/app/Filament/Resources/ShowcaseResource.php
@@ -109,6 +109,28 @@ public static function form(Schema $schema): Schema
->url()
->maxLength(255),
]),
+
+ Schemas\Components\Fieldset::make('Platform-specific Screenshots')
+ ->visible(fn (Schemas\Components\Utilities\Get $get) => $get('has_mobile') && $get('has_desktop'))
+ ->schema([
+ Forms\Components\FileUpload::make('mobile_screenshots')
+ ->label('Mobile Screenshots (optional, up to 5)')
+ ->image()
+ ->multiple()
+ ->maxFiles(5)
+ ->disk('public')
+ ->directory('showcase-screenshots')
+ ->reorderable(),
+
+ Forms\Components\FileUpload::make('desktop_screenshots')
+ ->label('Desktop Screenshots (optional, up to 5)')
+ ->image()
+ ->multiple()
+ ->maxFiles(5)
+ ->disk('public')
+ ->directory('showcase-screenshots')
+ ->reorderable(),
+ ]),
]),
Schemas\Components\Section::make('Certification')
diff --git a/app/Http/Controllers/ShowcaseController.php b/app/Http/Controllers/ShowcaseController.php
index 3bc767b4..0d0070b6 100644
--- a/app/Http/Controllers/ShowcaseController.php
+++ b/app/Http/Controllers/ShowcaseController.php
@@ -16,6 +16,8 @@ public function index(?string $platform = null): View
$query->withMobile();
} elseif ($platform === 'desktop') {
$query->withDesktop();
+ } elseif ($platform === 'both') {
+ $query->withMobile()->withDesktop();
}
$showcases = $query->paginate(10);
diff --git a/app/Livewire/ShowcaseSubmissionForm.php b/app/Livewire/ShowcaseSubmissionForm.php
index 454da2c7..5e2ef6c5 100644
--- a/app/Livewire/ShowcaseSubmissionForm.php
+++ b/app/Livewire/ShowcaseSubmissionForm.php
@@ -34,6 +34,16 @@ class ShowcaseSubmissionForm extends Component
public array $existingScreenshots = [];
+ #[Validate('nullable|array|max:5')]
+ public array $mobileScreenshots = [];
+
+ public array $existingMobileScreenshots = [];
+
+ #[Validate('nullable|array|max:5')]
+ public array $desktopScreenshots = [];
+
+ public array $existingDesktopScreenshots = [];
+
#[Validate('boolean')]
public bool $hasMobile = false;
@@ -67,6 +77,8 @@ public function mount(?Showcase $showcase = null): void
$this->description = $showcase->description;
$this->existingImage = $showcase->image;
$this->existingScreenshots = $showcase->screenshots ?? [];
+ $this->existingMobileScreenshots = $showcase->mobile_screenshots ?? [];
+ $this->existingDesktopScreenshots = $showcase->desktop_screenshots ?? [];
$this->hasMobile = $showcase->has_mobile;
$this->hasDesktop = $showcase->has_desktop;
$this->playStoreUrl = $showcase->play_store_url ?? '';
@@ -85,6 +97,8 @@ public function rules(): array
'description' => 'required|string|max:2000',
'image' => 'nullable|image|max:2048',
'screenshots.*' => 'nullable|image|max:2048',
+ 'mobileScreenshots.*' => 'nullable|image|max:2048',
+ 'desktopScreenshots.*' => 'nullable|image|max:2048',
'hasMobile' => 'boolean',
'hasDesktop' => 'boolean',
'playStoreUrl' => 'nullable|url|max:255',
@@ -115,11 +129,46 @@ public function removeExistingScreenshot(int $index): void
}
}
+ public function removeExistingMobileScreenshot(int $index): void
+ {
+ if (isset($this->existingMobileScreenshots[$index])) {
+ unset($this->existingMobileScreenshots[$index]);
+ $this->existingMobileScreenshots = array_values($this->existingMobileScreenshots);
+ }
+ }
+
+ public function removeExistingDesktopScreenshot(int $index): void
+ {
+ if (isset($this->existingDesktopScreenshots[$index])) {
+ unset($this->existingDesktopScreenshots[$index]);
+ $this->existingDesktopScreenshots = array_values($this->existingDesktopScreenshots);
+ }
+ }
+
public function removeExistingImage(): void
{
$this->existingImage = null;
}
+ /**
+ * @param array $existing
+ * @param array $uploads
+ * @return array
+ */
+ protected function storeScreenshots(array $existing, array $uploads): array
+ {
+ $paths = $existing;
+
+ foreach ($uploads as $upload) {
+ if (count($paths) >= 5) {
+ break;
+ }
+ $paths[] = $upload->store('showcase-screenshots', 'public');
+ }
+
+ return $paths;
+ }
+
public function submit(): mixed
{
$this->validate();
@@ -135,19 +184,19 @@ public function submit(): mixed
$imagePath = $this->image->store('showcase-images', 'public');
}
- $screenshotPaths = $this->existingScreenshots;
- foreach ($this->screenshots as $screenshot) {
- if (count($screenshotPaths) >= 5) {
- break;
- }
- $screenshotPaths[] = $screenshot->store('showcase-screenshots', 'public');
- }
+ $bothPlatforms = $this->hasMobile && $this->hasDesktop;
+
+ $screenshotPaths = $this->storeScreenshots($this->existingScreenshots, $this->screenshots);
+ $mobileScreenshotPaths = $bothPlatforms ? $this->storeScreenshots($this->existingMobileScreenshots, $this->mobileScreenshots) : [];
+ $desktopScreenshotPaths = $bothPlatforms ? $this->storeScreenshots($this->existingDesktopScreenshots, $this->desktopScreenshots) : [];
$data = [
'title' => $this->title,
'description' => $this->description,
'image' => $imagePath,
'screenshots' => $screenshotPaths ?: null,
+ 'mobile_screenshots' => $mobileScreenshotPaths ?: null,
+ 'desktop_screenshots' => $desktopScreenshotPaths ?: null,
'has_mobile' => $this->hasMobile,
'has_desktop' => $this->hasDesktop,
'play_store_url' => $this->hasMobile ? ($this->playStoreUrl ?: null) : null,
@@ -199,8 +248,8 @@ public function delete(): mixed
Storage::disk('public')->delete($this->showcase->image);
}
- if ($this->showcase->screenshots) {
- foreach ($this->showcase->screenshots as $screenshot) {
+ foreach ([$this->showcase->screenshots, $this->showcase->mobile_screenshots, $this->showcase->desktop_screenshots] as $screenshots) {
+ foreach ($screenshots ?? [] as $screenshot) {
Storage::disk('public')->delete($screenshot);
}
}
diff --git a/app/Models/Showcase.php b/app/Models/Showcase.php
index bc6c4e4d..cd8400b1 100644
--- a/app/Models/Showcase.php
+++ b/app/Models/Showcase.php
@@ -18,6 +18,8 @@ class Showcase extends Model
'description',
'image',
'screenshots',
+ 'mobile_screenshots',
+ 'desktop_screenshots',
'has_mobile',
'has_desktop',
'play_store_url',
@@ -60,6 +62,20 @@ public function needsReReview(): bool
return $this->approved_at !== null && $this->updated_at->isAfter($this->approved_at);
}
+ /**
+ * @return array
+ */
+ public function screenshotsFor(?string $platform): array
+ {
+ $override = match ($platform) {
+ 'mobile' => $this->mobile_screenshots,
+ 'desktop' => $this->desktop_screenshots,
+ default => null,
+ };
+
+ return ! empty($override) ? $override : ($this->screenshots ?? []);
+ }
+
#[Scope]
protected function approved(Builder $query): Builder
{
@@ -88,6 +104,8 @@ protected function casts(): array
{
return [
'screenshots' => 'array',
+ 'mobile_screenshots' => 'array',
+ 'desktop_screenshots' => 'array',
'has_mobile' => 'boolean',
'has_desktop' => 'boolean',
'certified_nativephp' => 'boolean',
diff --git a/database/factories/ShowcaseFactory.php b/database/factories/ShowcaseFactory.php
index e21a6254..60cbfd19 100644
--- a/database/factories/ShowcaseFactory.php
+++ b/database/factories/ShowcaseFactory.php
@@ -33,6 +33,8 @@ public function definition(): array
'description' => fake()->paragraph(3),
'image' => null,
'screenshots' => null,
+ 'mobile_screenshots' => null,
+ 'desktop_screenshots' => null,
'has_mobile' => $hasMobile,
'has_desktop' => $hasDesktop,
'play_store_url' => $hasMobile ? fake()->optional(0.7)->url() : null,
@@ -132,6 +134,20 @@ public function withWideScreenshots(int $count = 3): static
return $this->withScreenshots($count, tall: false);
}
+ public function withMobileScreenshots(int $count = 3): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'mobile_screenshots' => array_map(fn () => $this->generatePlaceholderScreenshot(tall: true), range(1, $count)),
+ ]);
+ }
+
+ public function withDesktopScreenshots(int $count = 3): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'desktop_screenshots' => array_map(fn () => $this->generatePlaceholderScreenshot(tall: false), range(1, $count)),
+ ]);
+ }
+
protected function generatePlaceholderScreenshot(bool $tall = false): string
{
$width = $tall ? 390 : 1280;
diff --git a/database/migrations/2026_08_18_120000_add_platform_screenshots_to_showcases_table.php b/database/migrations/2026_08_18_120000_add_platform_screenshots_to_showcases_table.php
new file mode 100644
index 00000000..cce33b51
--- /dev/null
+++ b/database/migrations/2026_08_18_120000_add_platform_screenshots_to_showcases_table.php
@@ -0,0 +1,29 @@
+json('mobile_screenshots')->nullable()->after('screenshots');
+ $table->json('desktop_screenshots')->nullable()->after('mobile_screenshots');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('showcases', function (Blueprint $table) {
+ $table->dropColumn(['mobile_screenshots', 'desktop_screenshots']);
+ });
+ }
+};
diff --git a/database/seeders/ShowcaseSeeder.php b/database/seeders/ShowcaseSeeder.php
index 50d187d6..f9ddb589 100644
--- a/database/seeders/ShowcaseSeeder.php
+++ b/database/seeders/ShowcaseSeeder.php
@@ -18,6 +18,7 @@ public function run(): void
Showcase::factory(2)->approved()->desktop()->withWideScreenshots(3)->create();
Showcase::factory(2)->approved()->desktop()->create();
Showcase::factory(2)->approved()->both()->withTallScreenshots(2)->create();
+ Showcase::factory(1)->approved()->both()->withMobileScreenshots(2)->withDesktopScreenshots(2)->create();
// Recently approved (will show as "new") with screenshots
Showcase::factory(2)->recentlyApproved()->mobile()->withTallScreenshots(4)->create();
diff --git a/resources/views/components/showcase-card.blade.php b/resources/views/components/showcase-card.blade.php
index 27490e9d..315fe52c 100644
--- a/resources/views/components/showcase-card.blade.php
+++ b/resources/views/components/showcase-card.blade.php
@@ -1,12 +1,16 @@
-@props(['showcase'])
+@props(['showcase', 'platform' => null])
+
+@php
+ $displayScreenshots = $showcase->screenshotsFor($platform);
+@endphp
- @if($showcase->screenshots && count($showcase->screenshots) > 0)
+ @if(count($displayScreenshots) > 0)
- @foreach($showcase->screenshots as $index => $screenshot)
+ @foreach($displayScreenshots as $index => $screenshot)
diff --git a/resources/views/showcase.blade.php b/resources/views/showcase.blade.php
index 19a59e58..25f6099c 100644
--- a/resources/views/showcase.blade.php
+++ b/resources/views/showcase.blade.php
@@ -1,4 +1,4 @@
-
+
{{-- Hero Section --}}
- Discover amazing {{ $platform ?? '' }} apps built by the NativePHP community. From productivity tools to creative applications, see what's possible with NativePHP.
+ Discover amazing {{ $platform === 'both' ? 'cross-platform' : ($platform ?? '') }} apps built by the NativePHP community. From productivity tools to creative applications, see what's possible with NativePHP.