Skip to content

Commit 2bc906d

Browse files
authored
Merge branch 'main' into separate-plugin-create-submit
2 parents 2f99e6a + 4727552 commit 2bc906d

25 files changed

+778
-23
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Plugin;
6+
use App\Models\User;
7+
use App\Notifications\NewPluginAvailable;
8+
use Illuminate\Console\Command;
9+
use Illuminate\Support\Facades\Notification;
10+
11+
class ResendNewPluginNotifications extends Command
12+
{
13+
protected $signature = 'plugins:resend-new-plugin-notifications
14+
{plugins* : Plugin names (vendor/package) to resend notifications for}
15+
{--dry-run : Preview what would happen without sending notifications}';
16+
17+
protected $description = 'Resend NewPluginAvailable notifications to opted-in users for specified plugins';
18+
19+
public function handle(): int
20+
{
21+
$pluginNames = $this->argument('plugins');
22+
$dryRun = $this->option('dry-run');
23+
24+
$plugins = Plugin::query()
25+
->whereIn('name', $pluginNames)
26+
->where('status', 'approved')
27+
->get();
28+
29+
$missingNames = collect($pluginNames)->diff($plugins->pluck('name'));
30+
31+
if ($missingNames->isNotEmpty()) {
32+
foreach ($missingNames as $name) {
33+
$this->error("Plugin not found or not approved: {$name}");
34+
}
35+
36+
return Command::FAILURE;
37+
}
38+
39+
$recipients = User::query()
40+
->where('receives_new_plugin_notifications', true)
41+
->whereNotIn('id', $plugins->pluck('user_id'))
42+
->get();
43+
44+
if ($recipients->isEmpty()) {
45+
$this->warn('No opted-in users found to notify.');
46+
47+
return Command::SUCCESS;
48+
}
49+
50+
$this->info("Plugins: {$plugins->pluck('name')->implode(', ')}");
51+
$this->info("Recipients: {$recipients->count()} opted-in users");
52+
53+
if ($dryRun) {
54+
$this->warn('[DRY RUN] No notifications will be sent.');
55+
56+
return Command::SUCCESS;
57+
}
58+
59+
foreach ($plugins as $plugin) {
60+
$pluginRecipients = $recipients->where('id', '!=', $plugin->user_id);
61+
62+
Notification::send($pluginRecipients, new NewPluginAvailable($plugin));
63+
64+
$this->info("Sent NewPluginAvailable for {$plugin->name} to {$pluginRecipients->count()} users.");
65+
}
66+
67+
$this->newLine();
68+
$this->info('Done. All notifications queued.');
69+
70+
return Command::SUCCESS;
71+
}
72+
}

app/Filament/Resources/PluginResource.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ public static function form(Schema $schema): Schema
7575
->suffixIconColor('gray'),
7676

7777
Forms\Components\Select::make('status')
78-
->options(PluginStatus::class),
78+
->options(PluginStatus::class)
79+
->disabled()
80+
->helperText('Use the Approve/Reject actions to change status'),
7981

8082
Forms\Components\Toggle::make('is_official')
8183
->label('Official (First-Party)')

app/Http/Controllers/ShowDocumentationController.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace App\Http\Controllers;
44

5+
use App\Services\DocsVersionService;
56
use App\Support\CommonMark\CommonMark;
7+
use Artesaos\SEOTools\Facades\SEOMeta;
68
use Artesaos\SEOTools\Facades\SEOTools;
79
use Illuminate\Contracts\View\Factory as ViewFactory;
810
use Illuminate\Http\RedirectResponse;
@@ -49,6 +51,13 @@ public function __invoke(Request $request, string $platform, string $version, ?s
4951
$title = $pageProperties['title'].' - NativePHP '.$platform.' v'.$version;
5052
$description = Arr::exists($pageProperties, 'description') ? $pageProperties['description'] : 'NativePHP documentation for '.$platform.' v'.$version;
5153

54+
$canonicalUrl = app(DocsVersionService::class)->determineCanonicalUrl(
55+
platform: $platform,
56+
page: $page,
57+
);
58+
59+
SEOMeta::setCanonical($canonicalUrl);
60+
5261
SEOTools::setTitle($title);
5362
SEOTools::setDescription($description);
5463

app/Notifications/NewPluginAvailable.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function toMail(object $notifiable): MailMessage
3535
->greeting('A new plugin is available!')
3636
->line("**{$this->plugin->name}** has just been added to the NativePHP Plugin Marketplace.")
3737
->action('View Plugin', route('plugins.show', $this->plugin->routeParams()))
38-
->line('You can manage your notification preferences in your account settings.');
38+
->line('[Manage your notification preferences]('.route('customer.settings', ['tab' => 'notifications']).').');
3939
}
4040

4141
/**
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
class DocsVersionService
6+
{
7+
public function determineCanonicalUrl(string $platform, string $page): string
8+
{
9+
$latestVersion = $platform === 'mobile' ? config('docs.latest_versions.mobile') : config('docs.latest_versions.desktop');
10+
11+
$page = $this->resolveOldVersionApisWithPluginsCorePage($platform, $latestVersion, $page);
12+
13+
$latestPagePath = resource_path("views/docs/{$platform}/{$latestVersion}/{$page}.md");
14+
15+
$page = file_exists($latestPagePath) ? $page : 'getting-started/introduction';
16+
17+
return route('docs.show', [
18+
'platform' => $platform,
19+
'version' => $latestVersion,
20+
'page' => $page,
21+
]);
22+
}
23+
24+
/**
25+
* Handle renamed paths (e.g., apis/* moved to plugins/core/*)
26+
*/
27+
public function resolveOldVersionApisWithPluginsCorePage(string $platform, string $latestVersion, string $page): string
28+
{
29+
if (str_starts_with($page, 'apis/')) {
30+
$remappedPage = 'plugins/core/'.substr($page, 5);
31+
$remappedPath = resource_path("views/docs/{$platform}/{$latestVersion}/{$remappedPage}.md");
32+
33+
if (file_exists($remappedPath)) {
34+
$page = $remappedPage;
35+
}
36+
}
37+
38+
return $page;
39+
}
40+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
export default (key) => ({
3+
init() {
4+
const storageKey = `sidebar_${key}`
5+
const saved = localStorage.getItem(storageKey)
6+
const hasCurrent = !!this.$el.querySelector('[data-current]')
7+
let skipNext = false
8+
9+
if ((hasCurrent || saved === 'open') && !this.$el.hasAttribute('data-open')) {
10+
if (hasCurrent) {
11+
skipNext = true
12+
}
13+
this.$el.querySelector('button')?.click()
14+
}
15+
16+
this._observer = new MutationObserver(() => {
17+
if (skipNext) {
18+
skipNext = false
19+
return
20+
}
21+
localStorage.setItem(storageKey, this.$el.hasAttribute('data-open') ? 'open' : 'closed')
22+
})
23+
this._observer.observe(this.$el, { attributes: true, attributeFilter: ['data-open'] })
24+
},
25+
destroy() {
26+
this._observer?.disconnect()
27+
},
28+
})

resources/js/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from '../../vendor/livewire/livewire/dist/livewire.esm.js'
99
import codeBlock from './alpine/codeBlock.js'
1010
import copyMarkdown from './alpine/copyMarkdown.js'
11+
import sidebarGroup from './alpine/sidebarGroup.js'
1112
import docsearch from '@docsearch/js'
1213
import Atropos from 'atropos'
1314
import '@docsearch/css'
@@ -62,6 +63,7 @@ window.gsap = gsap
6263
// Alpine
6364
Alpine.data('codeBlock', codeBlock)
6465
Alpine.data('copyMarkdown', copyMarkdown)
66+
Alpine.data('sidebarGroup', sidebarGroup)
6567
Alpine.magic('refAll', (el) => {
6668
return (refName) => {
6769
return Array.from(el.querySelectorAll(`[x-ref="${refName}"]`))

resources/views/components/blog/ad-rotation.blade.php

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@props(['ads' => ['mobile', 'devkit', 'ultra']])
1+
@props(['ads' => ['mobile', 'devkit', 'ultra', 'vibes', 'masterclass']])
22

33
@php
44
$adsJson = json_encode($ads);
@@ -159,4 +159,101 @@ class="absolute bottom-12 right-6 z-10 w-2.5 text-amber-100 animate-pulse "
159159
/>
160160
</a>
161161
@endif
162+
163+
{{-- The Vibes Ad --}}
164+
@if (in_array('vibes', $ads))
165+
<a
166+
x-show="ad === 'vibes'"
167+
x-cloak
168+
href="{{ route('the-vibes') }}"
169+
class="group relative z-0 grid place-items-center overflow-hidden rounded-2xl px-4 py-8 text-center text-pretty transition duration-200 hover:ring-1 hover:ring-violet-400"
170+
>
171+
{{-- Background image --}}
172+
<img
173+
src="{{ Vite::asset('resources/images/the-vibes/what-is-vibes.webp') }}"
174+
alt=""
175+
aria-hidden="true"
176+
class="absolute inset-0 -z-10 size-full object-cover blur-[1px] contrast-75 brightness-65"
177+
loading="lazy"
178+
/>
179+
180+
{{-- Title --}}
181+
<div class="z-10 text-lg font-bold text-white drop-shadow">
182+
The Vibes
183+
</div>
184+
185+
{{-- Tagline --}}
186+
<div class="z-10 mt-2 text-sm text-violet-100 drop-shadow">
187+
The unofficial Laracon US
188+
<strong class="text-white">Day 3</strong>
189+
</div>
190+
191+
{{-- CTA --}}
192+
<div class="z-10 mt-4 rounded-lg bg-white/20 px-4 py-1.5 text-sm font-medium text-white backdrop-blur-sm transition group-hover:bg-white/30">
193+
Grab Your Spot
194+
</div>
195+
196+
{{-- Scarcity Label --}}
197+
<div class="z-10 mt-2 text-xs text-violet-100 drop-shadow">
198+
Only 100 tickets!
199+
</div>
200+
201+
{{-- Decorative stars --}}
202+
<x-icons.star
203+
class="absolute top-4 right-3 z-10 w-3 -rotate-7 text-violet-300 animate-ping "
204+
/>
205+
<x-icons.star
206+
class="absolute top-8 left-4 z-10 w-2 rotate-12 text-indigo-300 animate-spin "
207+
/>
208+
</a>
209+
@endif
210+
211+
{{-- Masterclass Ad --}}
212+
@if (in_array('masterclass', $ads))
213+
<a
214+
x-show="ad === 'masterclass'"
215+
x-cloak
216+
href="{{ route('course') }}"
217+
class="group relative z-0 grid place-items-center overflow-hidden rounded-2xl bg-gradient-to-br from-emerald-500 to-teal-600 px-4 py-8 text-center text-pretty transition duration-200 hover:from-emerald-400 hover:to-teal-500 hover:ring-1 hover:ring-emerald-400"
218+
>
219+
{{-- Icon --}}
220+
<div class="grid size-14 place-items-center rounded-xl bg-white/20 text-white backdrop-blur-sm">
221+
<x-heroicon-s-academic-cap class="size-8" />
222+
</div>
223+
224+
{{-- Title --}}
225+
<div class="mt-3 text-lg font-bold text-white">
226+
The Masterclass
227+
</div>
228+
229+
{{-- Tagline --}}
230+
<div class="mt-2 text-sm text-emerald-50">
231+
Go from zero to
232+
<strong class="text-white">published app</strong>
233+
<br />
234+
in no time
235+
</div>
236+
237+
{{-- CTA --}}
238+
<div class="mt-4 rounded-lg bg-white/20 px-4 py-1.5 text-sm font-medium text-white backdrop-blur-sm transition group-hover:bg-white/30">
239+
Learn More
240+
</div>
241+
242+
{{-- Early Bird Label --}}
243+
<div class="mt-2 text-xs text-emerald-100">
244+
Early Bird Pricing
245+
</div>
246+
247+
{{-- Decorative stars --}}
248+
<x-icons.star
249+
class="absolute top-4 right-3 z-10 w-3 -rotate-7 text-emerald-200 animate-ping "
250+
/>
251+
<x-icons.star
252+
class="absolute top-8 left-4 z-10 w-2 rotate-12 text-teal-200 animate-spin "
253+
/>
254+
<x-icons.star
255+
class="absolute bottom-12 right-6 z-10 w-2.5 text-emerald-100 animate-pulse "
256+
/>
257+
</a>
258+
@endif
162259
</div>

resources/views/components/customer/plugin-credentials.blade.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
</div>
2626
<button
2727
type="button"
28-
x-data x-on:click="navigator.clipboard.writeText('composer config repositories.nativephp-plugins composer https://plugins.nativephp.com')"
28+
x-data="{ copied: false }"
29+
x-on:click="navigator.clipboard.writeText('composer config repositories.nativephp-plugins composer https://plugins.nativephp.com').then(() => { copied = true; setTimeout(() => copied = false, 2000) })"
2930
class="shrink-0 self-stretch bg-zinc-900 px-3 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200"
3031
title="Copy command"
3132
>
32-
<flux:icon name="clipboard" variant="outline" class="size-4" />
33+
<flux:icon x-show="!copied" name="clipboard" variant="outline" class="size-4" />
34+
<flux:icon x-show="copied" x-cloak name="check-circle" variant="outline" class="size-4 text-green-400" />
3335
</button>
3436
</div>
3537
<flux:text class="text-xs">2. Configure your credentials:</flux:text>
@@ -39,11 +41,13 @@ class="shrink-0 self-stretch bg-zinc-900 px-3 text-zinc-400 hover:bg-zinc-800 ho
3941
</div>
4042
<button
4143
type="button"
42-
x-data x-on:click="navigator.clipboard.writeText('composer config http-basic.plugins.nativephp.com {{ auth()->user()->email }} {{ $pluginLicenseKey }}')"
44+
x-data="{ copied: false }"
45+
x-on:click="navigator.clipboard.writeText('composer config http-basic.plugins.nativephp.com {{ auth()->user()->email }} {{ $pluginLicenseKey }}').then(() => { copied = true; setTimeout(() => copied = false, 2000) })"
4346
class="shrink-0 self-stretch bg-zinc-900 px-3 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200"
4447
title="Copy command"
4548
>
46-
<flux:icon name="clipboard" variant="outline" class="size-4" />
49+
<flux:icon x-show="!copied" name="clipboard" variant="outline" class="size-4" />
50+
<flux:icon x-show="copied" x-cloak name="check-circle" variant="outline" class="size-4 text-green-400" />
4751
</button>
4852
</div>
4953
</div>

resources/views/components/home/announcements.blade.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
{{-- Announcements Section: Plugins (full width) + Bifrost (50%) & Mimi/Jump (50% stacked) --}}
1+
{{-- Announcements Section: Plugins (full width) + Bifrost (50%) & Course/Jump (50% stacked) --}}
22
<section class="mt-4" aria-label="New announcements">
33
<div class="flex flex-col gap-4">
44
{{-- Plugins Announcement (Full Width) --}}
55
<x-home.plugins-announcement />
66

7-
{{-- Mimi & Jump + Bifrost Row --}}
7+
{{-- Course & Jump + Bifrost Row --}}
88
<div class="grid gap-4 xl:grid-cols-2">
9-
{{-- Left Column - Mimi & Jump --}}
9+
{{-- Left Column - Course & Jump --}}
1010
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-1">
11-
{{-- Mimi Card --}}
12-
<x-home.mimi-card />
11+
{{-- Course Card --}}
12+
<x-home.course-card />
1313

1414
{{-- Jump Card --}}
1515
<x-home.jump-card />

0 commit comments

Comments
 (0)