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
@if($platform) - No {{ $platform }} apps have been showcased yet. Be the first to submit yours! + No {{ $platform === 'both' ? 'cross-platform' : $platform }} apps have been showcased yet. Be the first to submit yours! @else The showcase is empty. Be the first to submit your NativePHP app! @endif diff --git a/routes/web.php b/routes/web.php index 390f62cd..be345430 100644 --- a/routes/web.php +++ b/routes/web.php @@ -216,7 +216,7 @@ Route::view('wall-of-love', 'wall-of-love')->name('wall-of-love'); Route::view('brand', 'brand')->name('brand'); Route::get('showcase/{platform?}', [ShowcaseController::class, 'index']) - ->where('platform', 'mobile|desktop') + ->where('platform', 'mobile|desktop|both') ->name('showcase'); Route::view('laracon-us-2025-giveaway', 'laracon-us-2025-giveaway')->name('laracon-us-2025-giveaway'); Route::view('privacy-policy', 'privacy-policy')->name('privacy-policy'); diff --git a/tests/Feature/ShowcasePageTest.php b/tests/Feature/ShowcasePageTest.php new file mode 100644 index 00000000..a06cda5c --- /dev/null +++ b/tests/Feature/ShowcasePageTest.php @@ -0,0 +1,111 @@ +approved()->mobile()->create(['title' => 'Mobile App']); + Showcase::factory()->approved()->desktop()->create(['title' => 'Desktop App']); + Showcase::factory()->pending()->create(['title' => 'Pending App']); + + $response = $this->get('/showcase'); + + $response->assertStatus(200); + $response->assertSee('Mobile App'); + $response->assertSee('Desktop App'); + $response->assertDontSee('Pending App'); + } + + public function test_mobile_filter_only_shows_apps_with_mobile_support(): void + { + Showcase::factory()->approved()->mobile()->create(['title' => 'Mobile Only App']); + Showcase::factory()->approved()->desktop()->create(['title' => 'Desktop Only App']); + Showcase::factory()->approved()->both()->create(['title' => 'Cross-Platform App']); + + $response = $this->get('/showcase/mobile'); + + $response->assertStatus(200); + $response->assertSee('Mobile Only App'); + $response->assertSee('Cross-Platform App'); + $response->assertDontSee('Desktop Only App'); + } + + public function test_desktop_filter_only_shows_apps_with_desktop_support(): void + { + Showcase::factory()->approved()->mobile()->create(['title' => 'Mobile Only App']); + Showcase::factory()->approved()->desktop()->create(['title' => 'Desktop Only App']); + Showcase::factory()->approved()->both()->create(['title' => 'Cross-Platform App']); + + $response = $this->get('/showcase/desktop'); + + $response->assertStatus(200); + $response->assertSee('Desktop Only App'); + $response->assertSee('Cross-Platform App'); + $response->assertDontSee('Mobile Only App'); + } + + public function test_both_filter_only_shows_apps_supporting_both_platforms(): void + { + Showcase::factory()->approved()->mobile()->create(['title' => 'Mobile Only App']); + Showcase::factory()->approved()->desktop()->create(['title' => 'Desktop Only App']); + Showcase::factory()->approved()->both()->create(['title' => 'Cross-Platform App']); + + $response = $this->get('/showcase/both'); + + $response->assertStatus(200); + $response->assertSee('Cross-Platform App'); + $response->assertDontSee('Mobile Only App'); + $response->assertDontSee('Desktop Only App'); + } + + public function test_invalid_platform_returns_404(): void + { + $response = $this->get('/showcase/tablet'); + + $response->assertStatus(404); + } + + public function test_mobile_and_desktop_filters_show_platform_specific_screenshots(): void + { + Storage::fake('public'); + + $showcase = Showcase::factory()->approved()->both() + ->withMobileScreenshots(1) + ->withDesktopScreenshots(1) + ->create(['title' => 'Split Screenshots App']); + + $mobileScreenshot = $showcase->mobile_screenshots[0]; + $desktopScreenshot = $showcase->desktop_screenshots[0]; + + $mobileResponse = $this->get('/showcase/mobile'); + $mobileResponse->assertSee($mobileScreenshot, false); + $mobileResponse->assertDontSee($desktopScreenshot, false); + + $desktopResponse = $this->get('/showcase/desktop'); + $desktopResponse->assertSee($desktopScreenshot, false); + $desktopResponse->assertDontSee($mobileScreenshot, false); + } + + public function test_falls_back_to_shared_screenshots_without_platform_override(): void + { + Storage::fake('public'); + + $showcase = Showcase::factory()->approved()->both()->withScreenshots(1)->create([ + 'title' => 'Shared Screenshots App', + ]); + + $sharedScreenshot = $showcase->screenshots[0]; + + $this->get('/showcase/mobile')->assertSee($sharedScreenshot, false); + $this->get('/showcase/desktop')->assertSee($sharedScreenshot, false); + } +} diff --git a/tests/Feature/ShowcaseSubmissionTest.php b/tests/Feature/ShowcaseSubmissionTest.php index 45ddc208..752d7093 100644 --- a/tests/Feature/ShowcaseSubmissionTest.php +++ b/tests/Feature/ShowcaseSubmissionTest.php @@ -7,8 +7,10 @@ use App\Models\User; use App\Notifications\ShowcaseSubmitted; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Http\UploadedFile; use Illuminate\Notifications\AnonymousNotifiable; use Illuminate\Support\Facades\Notification; +use Illuminate\Support\Facades\Storage; use Livewire\Livewire; use Tests\TestCase; @@ -123,4 +125,69 @@ public function test_showcase_notification_email_includes_submission_details(): $this->assertStringContainsString('sent back for review', $resubmitted); } + + public function test_submitting_both_platforms_stores_platform_specific_screenshots(): void + { + Storage::fake('public'); + + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(ShowcaseSubmissionForm::class) + ->set('title', 'Cross-Platform App') + ->set('description', 'Works everywhere.') + ->set('hasMobile', true) + ->set('hasDesktop', true) + ->set('mobileScreenshots', [UploadedFile::fake()->image('mobile.png')]) + ->set('desktopScreenshots', [UploadedFile::fake()->image('desktop.png')]) + ->set('certifiedNativephp', true) + ->call('submit') + ->assertHasNoErrors(); + + $showcase = Showcase::where('title', 'Cross-Platform App')->firstOrFail(); + $this->assertCount(1, $showcase->mobile_screenshots); + $this->assertCount(1, $showcase->desktop_screenshots); + Storage::disk('public')->assertExists($showcase->mobile_screenshots[0]); + Storage::disk('public')->assertExists($showcase->desktop_screenshots[0]); + } + + public function test_submitting_a_single_platform_never_stores_platform_specific_screenshots(): void + { + Storage::fake('public'); + + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(ShowcaseSubmissionForm::class) + ->set('title', 'Mobile Only App') + ->set('description', 'Just mobile.') + ->set('hasMobile', true) + ->set('certifiedNativephp', true) + ->call('submit') + ->assertHasNoErrors(); + + $showcase = Showcase::where('title', 'Mobile Only App')->firstOrFail(); + $this->assertNull($showcase->mobile_screenshots); + $this->assertNull($showcase->desktop_screenshots); + } + + public function test_unchecking_a_platform_clears_its_platform_specific_screenshots(): void + { + Storage::fake('public'); + + $user = User::factory()->create(); + $showcase = Showcase::factory()->both()->withMobileScreenshots(2)->withDesktopScreenshots(2)->create([ + 'user_id' => $user->id, + ]); + + Livewire::actingAs($user) + ->test(ShowcaseSubmissionForm::class, ['showcase' => $showcase]) + ->set('hasDesktop', false) + ->call('submit') + ->assertHasNoErrors(); + + $showcase->refresh(); + $this->assertNull($showcase->mobile_screenshots); + $this->assertNull($showcase->desktop_screenshots); + } } diff --git a/tests/Unit/ShowcaseTest.php b/tests/Unit/ShowcaseTest.php new file mode 100644 index 00000000..8dc8776e --- /dev/null +++ b/tests/Unit/ShowcaseTest.php @@ -0,0 +1,68 @@ + ['shared-1.png', 'shared-2.png']]); + + $this->assertSame(['shared-1.png', 'shared-2.png'], $showcase->screenshotsFor(null)); + } + + #[Test] + public function screenshots_for_falls_back_to_shared_screenshots_when_no_override_exists(): void + { + $showcase = new Showcase(['screenshots' => ['shared-1.png']]); + + $this->assertSame(['shared-1.png'], $showcase->screenshotsFor('mobile')); + $this->assertSame(['shared-1.png'], $showcase->screenshotsFor('desktop')); + } + + #[Test] + public function screenshots_for_falls_back_to_shared_screenshots_when_override_is_empty(): void + { + $showcase = new Showcase(['screenshots' => ['shared-1.png'], 'mobile_screenshots' => []]); + + $this->assertSame(['shared-1.png'], $showcase->screenshotsFor('mobile')); + } + + #[Test] + public function screenshots_for_returns_the_mobile_override_when_present(): void + { + $showcase = new Showcase([ + 'screenshots' => ['shared-1.png'], + 'mobile_screenshots' => ['mobile-1.png'], + 'desktop_screenshots' => ['desktop-1.png'], + ]); + + $this->assertSame(['mobile-1.png'], $showcase->screenshotsFor('mobile')); + } + + #[Test] + public function screenshots_for_returns_the_desktop_override_when_present(): void + { + $showcase = new Showcase([ + 'screenshots' => ['shared-1.png'], + 'mobile_screenshots' => ['mobile-1.png'], + 'desktop_screenshots' => ['desktop-1.png'], + ]); + + $this->assertSame(['desktop-1.png'], $showcase->screenshotsFor('desktop')); + } + + #[Test] + public function screenshots_for_returns_an_empty_array_when_nothing_is_set(): void + { + $showcase = new Showcase; + + $this->assertSame([], $showcase->screenshotsFor(null)); + $this->assertSame([], $showcase->screenshotsFor('mobile')); + } +}