From 8a3a7c1c603a83e97db9b75047e44f2c9568c8d6 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Sat, 6 Dec 2025 00:42:01 +0000 Subject: [PATCH 1/2] Showcase! --- app/Filament/Resources/ShowcaseResource.php | 242 ++++++++++++++++++ .../ShowcaseResource/Pages/CreateShowcase.php | 20 ++ .../ShowcaseResource/Pages/EditShowcase.php | 19 ++ .../ShowcaseResource/Pages/ListShowcases.php | 19 ++ .../CustomerShowcaseController.php | 31 +++ app/Http/Controllers/ShowcaseController.php | 28 ++ app/Livewire/ShowcaseSubmissionForm.php | 213 +++++++++++++++ app/Livewire/WallOfLoveBanner.php | 2 + app/Models/Showcase.php | 89 +++++++ database/factories/ShowcaseFactory.php | 109 ++++++++ ...25_12_05_131240_create_showcases_table.php | 44 ++++ database/seeders/ShowcaseSeeder.php | 30 +++ .../components/discounts-banner.blade.php | 6 +- .../navbar/device-dropdowns.blade.php | 27 +- .../views/components/navigation-bar.blade.php | 7 - .../views/components/showcase-card.blade.php | 216 ++++++++++++++++ .../views/customer/licenses/index.blade.php | 16 +- .../views/customer/showcase/create.blade.php | 63 +++++ .../views/customer/showcase/edit.blade.php | 59 +++++ .../views/customer/showcase/index.blade.php | 143 +++++++++++ .../tables/columns/platforms.blade.php | 8 + .../showcase-submission-form.blade.php | 211 +++++++++++++++ .../livewire/wall-of-love-banner.blade.php | 48 ++-- resources/views/showcase.blade.php | 177 +++++++++++++ routes/web.php | 11 +- 25 files changed, 1790 insertions(+), 48 deletions(-) create mode 100644 app/Filament/Resources/ShowcaseResource.php create mode 100644 app/Filament/Resources/ShowcaseResource/Pages/CreateShowcase.php create mode 100644 app/Filament/Resources/ShowcaseResource/Pages/EditShowcase.php create mode 100644 app/Filament/Resources/ShowcaseResource/Pages/ListShowcases.php create mode 100644 app/Http/Controllers/CustomerShowcaseController.php create mode 100644 app/Http/Controllers/ShowcaseController.php create mode 100644 app/Livewire/ShowcaseSubmissionForm.php create mode 100644 app/Models/Showcase.php create mode 100644 database/factories/ShowcaseFactory.php create mode 100644 database/migrations/2025_12_05_131240_create_showcases_table.php create mode 100644 database/seeders/ShowcaseSeeder.php create mode 100644 resources/views/components/showcase-card.blade.php create mode 100644 resources/views/customer/showcase/create.blade.php create mode 100644 resources/views/customer/showcase/edit.blade.php create mode 100644 resources/views/customer/showcase/index.blade.php create mode 100644 resources/views/filament/tables/columns/platforms.blade.php create mode 100644 resources/views/livewire/showcase-submission-form.blade.php create mode 100644 resources/views/showcase.blade.php diff --git a/app/Filament/Resources/ShowcaseResource.php b/app/Filament/Resources/ShowcaseResource.php new file mode 100644 index 00000000..5009fdc7 --- /dev/null +++ b/app/Filament/Resources/ShowcaseResource.php @@ -0,0 +1,242 @@ +schema([ + Forms\Components\Section::make('App Details') + ->schema([ + Forms\Components\Select::make('user_id') + ->label('Submitted By') + ->relationship('user', 'email') + ->getOptionLabelFromRecordUsing(fn ($record) => $record->name ? "{$record->name} ({$record->email})" : $record->email) + ->searchable(['name', 'email']) + ->preload() + ->required(), + + Forms\Components\TextInput::make('title') + ->required() + ->maxLength(255), + + Forms\Components\Textarea::make('description') + ->required() + ->rows(4), + + Forms\Components\FileUpload::make('image') + ->label('Main Image') + ->image() + ->disk('public') + ->directory('showcase-images'), + + Forms\Components\FileUpload::make('screenshots') + ->label('Screenshots (up to 5)') + ->image() + ->multiple() + ->maxFiles(5) + ->disk('public') + ->directory('showcase-screenshots') + ->reorderable(), + ]), + + Forms\Components\Section::make('Platform Availability') + ->schema([ + Forms\Components\Toggle::make('has_mobile') + ->label('Mobile App') + ->live(), + + Forms\Components\Toggle::make('has_desktop') + ->label('Desktop App') + ->live(), + + Forms\Components\Fieldset::make('Mobile Links') + ->visible(fn (Forms\Get $get) => $get('has_mobile')) + ->schema([ + Forms\Components\TextInput::make('app_store_url') + ->label('App Store URL') + ->url() + ->maxLength(255), + + Forms\Components\TextInput::make('play_store_url') + ->label('Play Store URL') + ->url() + ->maxLength(255), + ]), + + Forms\Components\Fieldset::make('Desktop Downloads') + ->visible(fn (Forms\Get $get) => $get('has_desktop')) + ->schema([ + Forms\Components\TextInput::make('windows_download_url') + ->label('Windows Download URL') + ->url() + ->maxLength(255), + + Forms\Components\TextInput::make('macos_download_url') + ->label('macOS Download URL') + ->url() + ->maxLength(255), + + Forms\Components\TextInput::make('linux_download_url') + ->label('Linux Download URL') + ->url() + ->maxLength(255), + ]), + ]), + + Forms\Components\Section::make('Certification') + ->schema([ + Forms\Components\Toggle::make('certified_nativephp') + ->label('Certified as built with NativePHP') + ->disabled(), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\ImageColumn::make('image') + ->label('Image') + ->disk('public') + ->height(40) + ->toggleable(), + + Tables\Columns\TextColumn::make('title') + ->searchable() + ->sortable(), + + Tables\Columns\ViewColumn::make('platforms') + ->label('Platforms') + ->view('filament.tables.columns.platforms'), + + Tables\Columns\TextColumn::make('approvedBy.name') + ->label('Approved By') + ->toggleable(), + + Tables\Columns\TextColumn::make('created_at') + ->label('Submitted') + ->dateTime() + ->sortable() + ->toggleable(), + + Tables\Columns\TextColumn::make('updated_at') + ->label('Updated') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + Tables\Filters\TernaryFilter::make('approved_at') + ->label('Status') + ->placeholder('All submissions') + ->trueLabel('Approved') + ->falseLabel('Pending') + ->queries( + true: fn (Builder $query) => $query->whereNotNull('approved_at'), + false: fn (Builder $query) => $query->whereNull('approved_at'), + ), + + Tables\Filters\TernaryFilter::make('has_mobile') + ->label('Mobile App') + ->placeholder('All') + ->trueLabel('Has Mobile') + ->falseLabel('No Mobile'), + + Tables\Filters\TernaryFilter::make('has_desktop') + ->label('Desktop App') + ->placeholder('All') + ->trueLabel('Has Desktop') + ->falseLabel('No Desktop'), + + Tables\Filters\Filter::make('needs_re_review') + ->label('Needs Re-Review') + ->query(fn (Builder $query): Builder => $query + ->whereNotNull('approved_at') + ->whereColumn('updated_at', '>', 'approved_at')), + ]) + ->actions([ + Tables\Actions\Action::make('approve') + ->icon('heroicon-o-check') + ->color('success') + ->visible(fn (Showcase $record) => $record->isPending()) + ->action(fn (Showcase $record) => $record->update([ + 'approved_at' => now(), + 'approved_by' => auth()->id(), + ])) + ->requiresConfirmation() + ->modalHeading('Approve Submission') + ->modalDescription('Are you sure you want to approve this app for the Showcase?'), + + Tables\Actions\Action::make('unapprove') + ->icon('heroicon-o-x-mark') + ->color('warning') + ->visible(fn (Showcase $record) => $record->isApproved()) + ->action(fn (Showcase $record) => $record->update([ + 'approved_at' => null, + 'approved_by' => null, + ])) + ->requiresConfirmation() + ->modalHeading('Unapprove Submission') + ->modalDescription('This will remove the app from the public Showcase.'), + + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\BulkAction::make('approve') + ->icon('heroicon-o-check') + ->color('success') + ->action(function ($records) { + $records->each(fn (Showcase $record) => $record->update([ + 'approved_at' => now(), + 'approved_by' => auth()->id(), + ])); + }) + ->requiresConfirmation() + ->modalHeading('Approve Selected Submissions') + ->modalDescription('Are you sure you want to approve all selected submissions?'), + + Tables\Actions\DeleteBulkAction::make(), + ]), + ]) + ->defaultSort('created_at', 'desc'); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListShowcases::route('/'), + 'create' => Pages\CreateShowcase::route('/create'), + 'edit' => Pages\EditShowcase::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/ShowcaseResource/Pages/CreateShowcase.php b/app/Filament/Resources/ShowcaseResource/Pages/CreateShowcase.php new file mode 100644 index 00000000..2a09b89e --- /dev/null +++ b/app/Filament/Resources/ShowcaseResource/Pages/CreateShowcase.php @@ -0,0 +1,20 @@ +id(); + + return $data; + } +} diff --git a/app/Filament/Resources/ShowcaseResource/Pages/EditShowcase.php b/app/Filament/Resources/ShowcaseResource/Pages/EditShowcase.php new file mode 100644 index 00000000..7badf7c8 --- /dev/null +++ b/app/Filament/Resources/ShowcaseResource/Pages/EditShowcase.php @@ -0,0 +1,19 @@ +user()->id) + ->latest() + ->get(); + + return view('customer.showcase.index', compact('showcases')); + } + + public function create(): View + { + return view('customer.showcase.create'); + } + + public function edit(Showcase $showcase): View + { + abort_if($showcase->user_id !== auth()->id(), 403); + + return view('customer.showcase.edit', compact('showcase')); + } +} diff --git a/app/Http/Controllers/ShowcaseController.php b/app/Http/Controllers/ShowcaseController.php new file mode 100644 index 00000000..3bc767b4 --- /dev/null +++ b/app/Http/Controllers/ShowcaseController.php @@ -0,0 +1,28 @@ +latest('approved_at'); + + if ($platform === 'mobile') { + $query->withMobile(); + } elseif ($platform === 'desktop') { + $query->withDesktop(); + } + + $showcases = $query->paginate(10); + + return view('showcase', [ + 'showcases' => $showcases, + 'platform' => $platform, + ]); + } +} diff --git a/app/Livewire/ShowcaseSubmissionForm.php b/app/Livewire/ShowcaseSubmissionForm.php new file mode 100644 index 00000000..018cc019 --- /dev/null +++ b/app/Livewire/ShowcaseSubmissionForm.php @@ -0,0 +1,213 @@ +exists && $showcase->user_id === auth()->id()) { + $this->showcase = $showcase; + $this->isEditing = true; + $this->title = $showcase->title; + $this->description = $showcase->description; + $this->existingImage = $showcase->image; + $this->existingScreenshots = $showcase->screenshots ?? []; + $this->hasMobile = $showcase->has_mobile; + $this->hasDesktop = $showcase->has_desktop; + $this->playStoreUrl = $showcase->play_store_url ?? ''; + $this->appStoreUrl = $showcase->app_store_url ?? ''; + $this->windowsDownloadUrl = $showcase->windows_download_url ?? ''; + $this->macosDownloadUrl = $showcase->macos_download_url ?? ''; + $this->linuxDownloadUrl = $showcase->linux_download_url ?? ''; + $this->certifiedNativephp = $showcase->certified_nativephp; + } + } + + public function rules(): array + { + $rules = [ + 'title' => 'required|string|max:255', + 'description' => 'required|string|max:2000', + 'image' => 'nullable|image|max:2048', + 'screenshots.*' => 'nullable|image|max:2048', + 'hasMobile' => 'boolean', + 'hasDesktop' => 'boolean', + 'playStoreUrl' => 'nullable|url|max:255', + 'appStoreUrl' => 'nullable|url|max:255', + 'windowsDownloadUrl' => 'nullable|url|max:255', + 'macosDownloadUrl' => 'nullable|url|max:255', + 'linuxDownloadUrl' => 'nullable|url|max:255', + 'certifiedNativephp' => 'accepted', + ]; + + return $rules; + } + + public function messages(): array + { + return [ + 'certifiedNativephp.accepted' => 'You must certify that your app is built with NativePHP.', + 'hasMobile.required_without' => 'Please select at least one platform (Mobile or Desktop).', + 'hasDesktop.required_without' => 'Please select at least one platform (Mobile or Desktop).', + ]; + } + + public function removeExistingScreenshot(int $index): void + { + if (isset($this->existingScreenshots[$index])) { + unset($this->existingScreenshots[$index]); + $this->existingScreenshots = array_values($this->existingScreenshots); + } + } + + public function removeExistingImage(): void + { + $this->existingImage = null; + } + + public function submit(): mixed + { + $this->validate(); + + if (! $this->hasMobile && ! $this->hasDesktop) { + $this->addError('hasMobile', 'Please select at least one platform (Mobile or Desktop).'); + + return null; + } + + $imagePath = $this->existingImage; + if ($this->image) { + $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'); + } + + $data = [ + 'title' => $this->title, + 'description' => $this->description, + 'image' => $imagePath, + 'screenshots' => $screenshotPaths ?: null, + 'has_mobile' => $this->hasMobile, + 'has_desktop' => $this->hasDesktop, + 'play_store_url' => $this->hasMobile ? ($this->playStoreUrl ?: null) : null, + 'app_store_url' => $this->hasMobile ? ($this->appStoreUrl ?: null) : null, + 'windows_download_url' => $this->hasDesktop ? ($this->windowsDownloadUrl ?: null) : null, + 'macos_download_url' => $this->hasDesktop ? ($this->macosDownloadUrl ?: null) : null, + 'linux_download_url' => $this->hasDesktop ? ($this->linuxDownloadUrl ?: null) : null, + 'certified_nativephp' => true, + ]; + + if ($this->isEditing && $this->showcase) { + $wasApproved = $this->showcase->isApproved(); + + $this->showcase->update($data); + + if ($wasApproved) { + $this->showcase->update([ + 'approved_at' => null, + 'approved_by' => null, + ]); + + return redirect()->route('customer.showcase.index') + ->with('warning', 'Your submission has been updated and sent back for review.'); + } + + return redirect()->route('customer.showcase.index') + ->with('success', 'Your submission has been updated.'); + } + + Showcase::create([ + 'user_id' => auth()->id(), + ...$data, + ]); + + return redirect()->route('customer.showcase.index') + ->with('success', 'Thank you! Your app has been submitted for review.'); + } + + public function delete(): mixed + { + if ($this->showcase && $this->showcase->user_id === auth()->id()) { + if ($this->showcase->image) { + Storage::disk('public')->delete($this->showcase->image); + } + + if ($this->showcase->screenshots) { + foreach ($this->showcase->screenshots as $screenshot) { + Storage::disk('public')->delete($screenshot); + } + } + + $this->showcase->delete(); + + return redirect()->route('customer.showcase.index') + ->with('success', 'Your submission has been deleted.'); + } + + return null; + } + + public function render() + { + return view('livewire.showcase-submission-form'); + } +} diff --git a/app/Livewire/WallOfLoveBanner.php b/app/Livewire/WallOfLoveBanner.php index 3041eed1..292ecd7d 100644 --- a/app/Livewire/WallOfLoveBanner.php +++ b/app/Livewire/WallOfLoveBanner.php @@ -6,6 +6,8 @@ class WallOfLoveBanner extends Component { + public bool $inline = false; + public function dismissBanner(): void { cache()->put('wall_of_love_dismissed_'.auth()->id(), true, now()->addWeek()); diff --git a/app/Models/Showcase.php b/app/Models/Showcase.php new file mode 100644 index 00000000..84e602d4 --- /dev/null +++ b/app/Models/Showcase.php @@ -0,0 +1,89 @@ + 'array', + 'has_mobile' => 'boolean', + 'has_desktop' => 'boolean', + 'certified_nativephp' => 'boolean', + 'approved_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function approvedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'approved_by'); + } + + public function isApproved(): bool + { + return $this->approved_at !== null; + } + + public function isPending(): bool + { + return $this->approved_at === null; + } + + public function isNew(): bool + { + return $this->approved_at !== null && $this->approved_at->isAfter(now()->subMonth()); + } + + public function needsReReview(): bool + { + return $this->approved_at !== null && $this->updated_at->isAfter($this->approved_at); + } + + public function scopeApproved(Builder $query): Builder + { + return $query->whereNotNull('approved_at'); + } + + public function scopePending(Builder $query): Builder + { + return $query->whereNull('approved_at'); + } + + public function scopeWithMobile(Builder $query): Builder + { + return $query->where('has_mobile', true); + } + + public function scopeWithDesktop(Builder $query): Builder + { + return $query->where('has_desktop', true); + } +} diff --git a/database/factories/ShowcaseFactory.php b/database/factories/ShowcaseFactory.php new file mode 100644 index 00000000..8bd33cc8 --- /dev/null +++ b/database/factories/ShowcaseFactory.php @@ -0,0 +1,109 @@ + + */ +class ShowcaseFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $hasMobile = fake()->boolean(50); + $hasDesktop = fake()->boolean(50); + + if (! $hasMobile && ! $hasDesktop) { + $hasMobile = fake()->boolean(); + $hasDesktop = ! $hasMobile; + } + + return [ + 'user_id' => User::factory(), + 'title' => fake()->words(rand(2, 4), true), + 'description' => fake()->paragraph(3), + 'image' => null, + 'screenshots' => null, + 'has_mobile' => $hasMobile, + 'has_desktop' => $hasDesktop, + 'play_store_url' => $hasMobile ? fake()->optional(0.7)->url() : null, + 'app_store_url' => $hasMobile ? fake()->optional(0.7)->url() : null, + 'windows_download_url' => $hasDesktop ? fake()->optional(0.6)->url() : null, + 'macos_download_url' => $hasDesktop ? fake()->optional(0.6)->url() : null, + 'linux_download_url' => $hasDesktop ? fake()->optional(0.5)->url() : null, + 'certified_nativephp' => true, + 'approved_at' => null, + 'approved_by' => null, + ]; + } + + public function approved(): static + { + return $this->state(fn (array $attributes) => [ + 'approved_at' => fake()->dateTimeBetween('-60 days', 'now'), + 'approved_by' => User::factory(), + ]); + } + + public function recentlyApproved(): static + { + return $this->state(fn (array $attributes) => [ + 'approved_at' => fake()->dateTimeBetween('-25 days', 'now'), + 'approved_by' => User::factory(), + ]); + } + + public function pending(): static + { + return $this->state(fn (array $attributes) => [ + 'approved_at' => null, + 'approved_by' => null, + ]); + } + + public function mobile(): static + { + return $this->state(fn (array $attributes) => [ + 'has_mobile' => true, + 'has_desktop' => false, + 'play_store_url' => fake()->optional(0.7)->url(), + 'app_store_url' => fake()->optional(0.7)->url(), + 'windows_download_url' => null, + 'macos_download_url' => null, + 'linux_download_url' => null, + ]); + } + + public function desktop(): static + { + return $this->state(fn (array $attributes) => [ + 'has_mobile' => false, + 'has_desktop' => true, + 'play_store_url' => null, + 'app_store_url' => null, + 'windows_download_url' => fake()->optional(0.6)->url(), + 'macos_download_url' => fake()->optional(0.6)->url(), + 'linux_download_url' => fake()->optional(0.5)->url(), + ]); + } + + public function both(): static + { + return $this->state(fn (array $attributes) => [ + 'has_mobile' => true, + 'has_desktop' => true, + 'play_store_url' => fake()->optional(0.7)->url(), + 'app_store_url' => fake()->optional(0.7)->url(), + 'windows_download_url' => fake()->optional(0.6)->url(), + 'macos_download_url' => fake()->optional(0.6)->url(), + 'linux_download_url' => fake()->optional(0.5)->url(), + ]); + } +} diff --git a/database/migrations/2025_12_05_131240_create_showcases_table.php b/database/migrations/2025_12_05_131240_create_showcases_table.php new file mode 100644 index 00000000..1cf41ab1 --- /dev/null +++ b/database/migrations/2025_12_05_131240_create_showcases_table.php @@ -0,0 +1,44 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('title'); + $table->text('description'); + $table->string('image')->nullable(); + $table->json('screenshots')->nullable(); + $table->boolean('has_mobile')->default(false); + $table->boolean('has_desktop')->default(false); + $table->string('play_store_url')->nullable(); + $table->string('app_store_url')->nullable(); + $table->string('windows_download_url')->nullable(); + $table->string('macos_download_url')->nullable(); + $table->string('linux_download_url')->nullable(); + $table->boolean('certified_nativephp')->default(false); + $table->timestamp('approved_at')->nullable(); + $table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->index(['approved_at', 'has_mobile', 'has_desktop']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('showcases'); + } +}; diff --git a/database/seeders/ShowcaseSeeder.php b/database/seeders/ShowcaseSeeder.php new file mode 100644 index 00000000..40cbeb81 --- /dev/null +++ b/database/seeders/ShowcaseSeeder.php @@ -0,0 +1,30 @@ +approved()->mobile()->create(); + Showcase::factory(4)->approved()->desktop()->create(); + Showcase::factory(2)->approved()->both()->create(); + + // 5 recently approved (will show as "new") + Showcase::factory(2)->recentlyApproved()->mobile()->create(); + Showcase::factory(2)->recentlyApproved()->desktop()->create(); + Showcase::factory(1)->recentlyApproved()->both()->create(); + + // 5 pending review + Showcase::factory(2)->pending()->mobile()->create(); + Showcase::factory(2)->pending()->desktop()->create(); + Showcase::factory(1)->pending()->both()->create(); + } +} diff --git a/resources/views/components/discounts-banner.blade.php b/resources/views/components/discounts-banner.blade.php index 26a6fe6e..0837b561 100644 --- a/resources/views/components/discounts-banner.blade.php +++ b/resources/views/components/discounts-banner.blade.php @@ -1,5 +1,7 @@ -
-
+@props(['inline' => false]) + +
!$inline])> +
diff --git a/resources/views/components/navbar/device-dropdowns.blade.php b/resources/views/components/navbar/device-dropdowns.blade.php index 320d676b..87bf65a7 100644 --- a/resources/views/components/navbar/device-dropdowns.blade.php +++ b/resources/views/components/navbar/device-dropdowns.blade.php @@ -1,3 +1,7 @@ +@php + $showShowcase = \App\Models\Showcase::approved()->count() >= 4; +@endphp +
- {{-- Wall of Love Callout for Early Adopters --}} - - - + {{-- Banners --}} +
+
+ + +
+
{{-- Content --}}
diff --git a/resources/views/customer/showcase/create.blade.php b/resources/views/customer/showcase/create.blade.php new file mode 100644 index 00000000..559f8437 --- /dev/null +++ b/resources/views/customer/showcase/create.blade.php @@ -0,0 +1,63 @@ + +
+ {{-- Header --}} +
+
+
+ {{-- Breadcrumb --}} + + +
+

Submit Your App to the Showcase

+

+ Share your NativePHP app with the community! Your submission will be reviewed by our team. +

+
+
+
+
+ + {{-- Content --}} +
+
+
+ {{-- Info Box --}} +
+
+
+

+ Showcase Guidelines +

+
    +
  • Your app must be built with NativePHP
  • +
  • Include clear screenshots showcasing your app
  • +
  • Provide download links or store URLs where users can get your app
  • +
  • Submissions are reviewed before being published
  • +
+
+
+
+ + {{-- Submission Form --}} + +
+
+
+
+
diff --git a/resources/views/customer/showcase/edit.blade.php b/resources/views/customer/showcase/edit.blade.php new file mode 100644 index 00000000..f4888849 --- /dev/null +++ b/resources/views/customer/showcase/edit.blade.php @@ -0,0 +1,59 @@ + +
+ {{-- Header --}} +
+
+
+ {{-- Breadcrumb --}} + + +
+
+

Edit Your Submission

+

+ Update the details of your showcase submission. +

+
+
+ @if($showcase->isApproved()) + + Approved + + @else + + Pending Review + + @endif +
+
+
+
+
+ + {{-- Content --}} +
+
+
+ {{-- Submission Form --}} + +
+
+
+
+
diff --git a/resources/views/customer/showcase/index.blade.php b/resources/views/customer/showcase/index.blade.php new file mode 100644 index 00000000..58b9ce03 --- /dev/null +++ b/resources/views/customer/showcase/index.blade.php @@ -0,0 +1,143 @@ + +
+ {{-- Header --}} +
+
+
+
+ +

Your Showcase Submissions

+

+ Submit your NativePHP apps to be featured on our showcase +

+
+ +
+
+
+ + {{-- Messages --}} +
+ @if(session()->has('success')) +
+
+ + + +

{{ session('success') }}

+
+
+ @endif + + @if(session()->has('warning')) +
+
+ + + +

{{ session('warning') }}

+
+
+ @endif +
+ + {{-- Content --}} +
+ @if($showcases->count() > 0) + + @else +
+
+ + + +

No submissions yet

+

+ Get started by submitting your first NativePHP app to the showcase. +

+ +
+
+ @endif +
+
+
diff --git a/resources/views/filament/tables/columns/platforms.blade.php b/resources/views/filament/tables/columns/platforms.blade.php new file mode 100644 index 00000000..07e28213 --- /dev/null +++ b/resources/views/filament/tables/columns/platforms.blade.php @@ -0,0 +1,8 @@ +
+ @if($getRecord()->has_mobile) + + @endif + @if($getRecord()->has_desktop) + + @endif +
diff --git a/resources/views/livewire/showcase-submission-form.blade.php b/resources/views/livewire/showcase-submission-form.blade.php new file mode 100644 index 00000000..771ec43f --- /dev/null +++ b/resources/views/livewire/showcase-submission-form.blade.php @@ -0,0 +1,211 @@ +
+ {{-- Warning for approved submissions being edited --}} + @if ($isEditing && $showcase?->isApproved()) +
+
+
+ + + +
+
+

Re-review Required

+

+ This submission is currently approved. If you make changes, it will need to be reviewed again before appearing in the showcase. +

+
+
+
+ @endif + + {{-- Title Field --}} +
+ + + @error('title')

{{ $message }}

@enderror +
+ + {{-- Description Field --}} +
+ + + @error('description')

{{ $message }}

@enderror +

Maximum 2000 characters.

+
+ + {{-- Main Image Field --}} +
+ + @if ($existingImage) +
+ Current image + +
+ @endif +
+ +
+ @error('image')

{{ $message }}

@enderror +

Max 2MB. Recommended: Square image, at least 256x256px.

+
+ + {{-- Screenshots Field --}} +
+ + @if (count($existingScreenshots) > 0) +
+ @foreach ($existingScreenshots as $index => $screenshot) +
+ Screenshot {{ $index + 1 }} + +
+ @endforeach +
+ @endif + @if (count($existingScreenshots) < 5) +
+ +
+

+ Max 2MB each. You can add {{ 5 - count($existingScreenshots) }} more screenshot(s). +

+ @endif + @error('screenshots.*')

{{ $message }}

@enderror +
+ + {{-- Platform Selection --}} +
+ + +
+ {{-- Mobile Toggle --}} +
+
+ +
+
+ +

iOS and/or Android

+
+
+ + {{-- Mobile Links (shown when hasMobile is true) --}} + @if ($hasMobile) +
+
+ + + @error('appStoreUrl')

{{ $message }}

@enderror +
+ +
+ + + @error('playStoreUrl')

{{ $message }}

@enderror +
+
+ @endif + + {{-- Desktop Toggle --}} +
+
+ +
+
+ +

Windows, macOS, and/or Linux

+
+
+ + {{-- Desktop Links (shown when hasDesktop is true) --}} + @if ($hasDesktop) +
+
+ + + @error('windowsDownloadUrl')

{{ $message }}

@enderror +
+ +
+ + + @error('macosDownloadUrl')

{{ $message }}

@enderror +
+ +
+ + + @error('linuxDownloadUrl')

{{ $message }}

@enderror +
+
+ @endif +
+ + @error('hasMobile')

{{ $message }}

@enderror +
+ + {{-- Certification Checkbox --}} +
+
+
+ +
+
+ +

+ By checking this box, you confirm that your application is built using NativePHP. + Submissions found not to be built with NativePHP may be rejected or removed from the showcase. +

+
+
+ @error('certifiedNativephp')

{{ $message }}

@enderror +
+ + {{-- Form Actions --}} +
+
+ @if ($isEditing) + + @endif +
+
+ + Cancel + + +
+
+
diff --git a/resources/views/livewire/wall-of-love-banner.blade.php b/resources/views/livewire/wall-of-love-banner.blade.php index 9f5e5079..77f6e7de 100644 --- a/resources/views/livewire/wall-of-love-banner.blade.php +++ b/resources/views/livewire/wall-of-love-banner.blade.php @@ -1,29 +1,27 @@ -
+
!$inline])> @if($this->shouldShowBanner()) -
-
-
-
- - - -
-
-

- Join our Wall of Love! -

-

- As an early adopter who purchased a license before June 1st, 2025, we'd love to feature you on - our Wall of Love page. -

-
- - Submit Your Details - - -
+
+
+
+ + + +
+
+

+ Join our Wall of Love! +

+

+ As an early adopter who purchased a license before June 1st, 2025, we'd love to feature you on + our Wall of Love page. +

+
+ + Submit Your Details + +
diff --git a/resources/views/showcase.blade.php b/resources/views/showcase.blade.php new file mode 100644 index 00000000..19a59e58 --- /dev/null +++ b/resources/views/showcase.blade.php @@ -0,0 +1,177 @@ + + {{-- Hero Section --}} +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Primary Heading --}} +

+
+
+ @if($platform === 'mobile') + Mobile + @elseif($platform === 'desktop') + Desktop + @else + App + @endif +
+
+
+
+ Showcase +
+
+

+ +
+
+
+
+
+
+ + {{-- Description --}} +

+ Discover amazing {{ $platform ?? '' }} apps built by the NativePHP community. From productivity tools to creative applications, see what's possible with NativePHP. +

+ + {{-- Platform Filter --}} + +
+
+ + {{-- Showcase Grid --}} +
+ @if ($showcases->count() > 0) +
+ @foreach ($showcases as $showcase) + + @endforeach +
+ + {{-- Pagination --}} + @if ($showcases->hasPages()) +
+ {{ $showcases->links() }} +
+ @endif + @else +
+
+
🚀
+

+ No Apps Yet +

+

+ @if($platform) + No {{ $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 +

+
+
+ @endif +
+ + {{-- CTA Section --}} +
+
+

+ Built something with NativePHP? +

+

+ We'd love to feature your app in our showcase. Share your creation with the NativePHP community! +

+ @auth + + Submit Your App + + @else + + Log in to Submit + + @endauth +
+
+
diff --git a/routes/web.php b/routes/web.php index f538e489..124da435 100644 --- a/routes/web.php +++ b/routes/web.php @@ -38,6 +38,9 @@ Route::view('alt-pricing', 'alt-pricing')->name('alt-pricing')->middleware('signed'); Route::view('wall-of-love', 'wall-of-love')->name('wall-of-love'); Route::view('brand', 'brand')->name('brand'); +Route::get('showcase/{platform?}', [App\Http\Controllers\ShowcaseController::class, 'index']) + ->where('platform', 'mobile|desktop') + ->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'); Route::view('terms-of-service', 'terms-of-service')->name('terms-of-service'); @@ -115,7 +118,7 @@ Route::get('callback', function (Illuminate\Http\Request $request) { $url = $request->query('url'); - if ($url && !str_starts_with($url, 'http')) { + if ($url && ! str_starts_with($url, 'http')) { return redirect()->away($url.'?token='.uuid_create()); } @@ -131,6 +134,11 @@ // Wall of Love submission Route::get('wall-of-love/create', [App\Http\Controllers\WallOfLoveSubmissionController::class, 'create'])->name('wall-of-love.create'); + // Showcase submissions + Route::get('showcase', [App\Http\Controllers\CustomerShowcaseController::class, 'index'])->name('showcase.index'); + Route::get('showcase/create', [App\Http\Controllers\CustomerShowcaseController::class, 'create'])->name('showcase.create'); + Route::get('showcase/{showcase}/edit', [App\Http\Controllers\CustomerShowcaseController::class, 'edit'])->name('showcase.edit'); + // Billing portal Route::get('billing-portal', function (Illuminate\Http\Request $request) { $user = $request->user(); @@ -151,5 +159,4 @@ Route::post('licenses/{licenseKey}/sub-licenses/{subLicense}/send-email', [CustomerSubLicenseController::class, 'sendEmail'])->name('licenses.sub-licenses.send-email'); }); - Route::get('.well-known/assetlinks.json', [ApplinksController::class, 'assetLinks']); From d8af28f9ab755dfe3bfbe99226d3ca4c9e3dd2ee Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Sat, 6 Dec 2025 00:44:32 +0000 Subject: [PATCH 2/2] style --- app/Http/Controllers/ApplinksController.php | 2 -- config/docs.php | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/Http/Controllers/ApplinksController.php b/app/Http/Controllers/ApplinksController.php index e270a22b..94f399e8 100644 --- a/app/Http/Controllers/ApplinksController.php +++ b/app/Http/Controllers/ApplinksController.php @@ -2,8 +2,6 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; - class ApplinksController extends Controller { public function assetLinks() diff --git a/config/docs.php b/config/docs.php index f50cdaa6..47e5dc63 100644 --- a/config/docs.php +++ b/config/docs.php @@ -18,4 +18,4 @@ 'mobile' => 2, ], -]; \ No newline at end of file +];