Initial baseline: LaraBlog core with plugin commerce surface.

Captures the current working tree after theme slots, ArticleAccess, and the payment / paid-content plugins so subsequent work has a reviewable git history.
This commit is contained in:
ak
2026-08-12 01:15:38 +08:00
commit 263b98b218
337 changed files with 31393 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Domain\Plugin\PluginManager;
use App\Models\Plugin;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Support\Icons\Heroicon;
use UnitEnum;
class ManagePlugins extends Page
{
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedPuzzlePiece;
protected static ?int $navigationSort = 81;
protected string $view = 'filament.pages.manage-plugins';
/** @var array<int, array<string, mixed>> */
public array $plugins = [];
public static function getNavigationGroup(): ?string
{
return __('admin.groups.system');
}
public static function getNavigationLabel(): string
{
return __('admin.nav.plugins');
}
public function getTitle(): string
{
return __('admin.pages.plugins_title');
}
public function mount(PluginManager $manager): void
{
$manager->syncDiscoveredPlugins();
$this->reload($manager);
}
public function enable(string $name, PluginManager $manager): void
{
try {
$manager->enable($name);
$this->reload($manager);
Notification::make()->title(__('admin.pages.enable').''.$name)->success()->send();
} catch (\Throwable $e) {
Notification::make()->title($e->getMessage())->danger()->send();
}
}
public function disable(string $name, PluginManager $manager): void
{
try {
$manager->disable($name);
$this->reload($manager);
Notification::make()->title(__('admin.pages.disable').''.$name)->success()->send();
} catch (\Throwable $e) {
Notification::make()->title($e->getMessage())->danger()->send();
}
}
public function showDocs(string $name, PluginManager $manager): void
{
$docs = $manager->readDocs($name);
if ($docs === null || trim($docs) === '') {
Notification::make()->title(__('admin.messages.plugin_docs_missing'))->warning()->send();
return;
}
Notification::make()
->title(__('admin.pages.plugin_docs'))
->body(str($docs)->limit(1800)->toString())
->persistent()
->info()
->send();
}
protected function reload(PluginManager $manager): void
{
$discovered = $manager->discover();
$records = Plugin::query()->get()->keyBy('name');
$accents = ['#0f8a7a', '#c45c26', '#2563eb', '#7c3aed', '#db2777', '#0891b2'];
$this->plugins = $discovered->values()->map(function (array $manifest, int $index) use ($records, $accents, $manager) {
$name = (string) $manifest['name'];
$titleKey = 'admin.plugins.'.$name.'.title';
$descKey = 'admin.plugins.'.$name.'.description';
$record = $records->get($name);
$requires = (array) ($manifest['requires'] ?? []);
return [
'name' => $name,
'title' => __($titleKey) !== $titleKey ? __($titleKey) : ($manifest['title'] ?? $name),
'version' => $manifest['version'] ?? '1.0.0',
'description' => __($descKey) !== $descKey ? __($descKey) : ($manifest['description'] ?? ''),
'enabled' => (bool) ($record?->enabled),
'requires' => $requires,
'has_docs' => $manager->docsPath($name) !== null,
'accent' => $accents[$index % count($accents)],
];
})->all();
}
protected function getHeaderActions(): array
{
return [
Action::make('sync')
->label(__('admin.pages.plugins_sync'))
->action(function (PluginManager $manager): void {
$manager->syncDiscoveredPlugins();
$this->reload($manager);
Notification::make()->title(__('admin.pages.plugins_sync'))->success()->send();
}),
];
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Domain\Theme\ThemeManager;
use App\Settings\GeneralSettings;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Artisan;
use UnitEnum;
class ManageThemes extends Page
{
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedSwatch;
protected static ?int $navigationSort = 80;
protected string $view = 'filament.pages.manage-themes';
public string $activeTheme = 'default';
/** @var array<int, array<string, mixed>> */
public array $themes = [];
public static function getNavigationGroup(): ?string
{
return __('admin.groups.system');
}
public static function getNavigationLabel(): string
{
return __('admin.nav.themes');
}
public function getTitle(): string
{
return __('admin.pages.themes_title');
}
public function mount(ThemeManager $themes, GeneralSettings $settings): void
{
$this->reload($themes, $settings);
}
public function activate(string $slug, ThemeManager $themes, GeneralSettings $settings): void
{
$themes->setActive($slug);
Artisan::call('themes:publish', ['theme' => $slug]);
$this->reload($themes, $settings);
$report = $themes->slotReport($slug);
$notification = Notification::make()->title(__('admin.pages.activate').''.$slug);
if (($report['status'] ?? '') === 'full') {
$notification->success()->send();
return;
}
$notification
->warning()
->body(__('admin.messages.theme_slots_warn', [
'label' => $report['label'] ?? __('admin.slots.status_undeclared'),
]))
->send();
}
protected function reload(ThemeManager $themes, GeneralSettings $settings): void
{
$this->activeTheme = $settings->active_theme ?: 'default';
$this->themes = $themes->discover()->map(function (array $theme) use ($themes) {
$slug = (string) ($theme['slug'] ?? $theme['name'] ?? '');
$titleKey = 'admin.themes.'.$slug.'.title';
$descKey = 'admin.themes.'.$slug.'.description';
$previewFile = base_path('themes/'.$slug.'/assets/preview.svg');
$report = $theme['slot_report'] ?? $themes->slotReport($slug);
return [
'slug' => $slug,
'title' => __($titleKey) !== $titleKey ? __($titleKey) : ($theme['title'] ?? $slug),
'description' => __($descKey) !== $descKey ? __($descKey) : ($theme['description'] ?? ''),
'version' => $theme['version'] ?? '1.0.0',
'preview' => is_file($previewFile)
? url('/themes/'.$slug.'/preview.svg').'?v='.filemtime($previewFile)
: null,
'slots_status' => $report['status'] ?? 'undeclared',
'slots_label' => $report['label'] ?? __('admin.slots.status_undeclared'),
'slots_missing' => $report['missing_standard'] ?? [],
'slots_declared_count' => count($report['declared'] ?? []),
];
})->values()->all();
}
protected function getHeaderActions(): array
{
return [
Action::make('refresh')
->label(__('admin.pages.themes_refresh'))
->action(fn (ThemeManager $themes, GeneralSettings $settings) => $this->reload($themes, $settings)),
Action::make('publish')
->label(__('admin.pages.themes_publish'))
->action(function (): void {
Artisan::call('themes:publish');
Notification::make()->title(__('admin.pages.themes_publish'))->success()->send();
}),
];
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use BackedEnum;
use Filament\Support\Icons\Heroicon;
class MembershipPluginPage extends PluginSkeletonPage
{
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedIdentification;
protected static ?int $navigationSort = 102;
protected static function pluginName(): string
{
return 'larablog/membership';
}
protected static function navKey(): string
{
return 'membership';
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use BackedEnum;
use Filament\Support\Icons\Heroicon;
class PaymentPluginPage extends PluginSkeletonPage
{
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCreditCard;
protected static ?int $navigationSort = 101;
protected static function pluginName(): string
{
return 'larablog/payment';
}
protected static function navKey(): string
{
return 'payment';
}
public static function shouldRegisterNavigation(): bool
{
// UI is owned by plugins/larablog/payment Filament resources/pages.
return false;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use BackedEnum;
use Filament\Support\Icons\Heroicon;
class PluginMarketplacePage extends PluginSkeletonPage
{
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedShoppingBag;
protected static ?int $navigationSort = 103;
protected static function pluginName(): string
{
return 'larablog/plugin-marketplace';
}
protected static function navKey(): string
{
return 'plugin_marketplace';
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Models\Plugin;
use BackedEnum;
use Filament\Pages\Page;
abstract class PluginSkeletonPage extends Page
{
protected static ?int $navigationSort = 100;
protected string $view = 'filament.pages.plugin-skeleton';
abstract protected static function pluginName(): string;
abstract protected static function navKey(): string;
public static function getNavigationGroup(): ?string
{
return __('admin.groups.plugins');
}
public static function getNavigationLabel(): string
{
return __('admin.nav.'.static::navKey());
}
public function getTitle(): string
{
return static::translatedTitle();
}
public function getHeading(): string
{
return static::translatedTitle();
}
protected static function translatedTitle(): string
{
$key = 'admin.plugins.'.static::pluginName().'.title';
return __($key) !== $key ? __($key) : __('admin.nav.'.static::navKey());
}
protected static function translatedDescription(): string
{
$key = 'admin.plugins.'.static::pluginName().'.description';
return __($key) !== $key ? __($key) : '';
}
public static function shouldRegisterNavigation(): bool
{
return Plugin::query()
->where('name', static::pluginName())
->where('enabled', true)
->exists();
}
public function getViewData(): array
{
return [
'pluginName' => static::pluginName(),
'pluginTitle' => static::translatedTitle(),
'pluginDescription' => static::translatedDescription(),
'enabled' => Plugin::query()
->where('name', static::pluginName())
->where('enabled', true)
->exists(),
];
}
}
+357
View File
@@ -0,0 +1,357 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Domain\Blog\ContentFormat;
use App\Domain\Theme\ThemeSlot;
use App\Settings\AiSettings;
use App\Settings\BlogSettings;
use App\Settings\CommentSettings;
use App\Settings\GeneralSettings;
use App\Settings\SeoSettings;
use App\Settings\SnippetSettings;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\EmbeddedSchema;
use Filament\Schemas\Components\Form;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
/**
* @property-read Schema $form
*/
class SiteSettings extends Page
{
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCog6Tooth;
protected static ?int $navigationSort = 85;
public static function getNavigationGroup(): ?string
{
return __('admin.groups.system');
}
public static function getNavigationLabel(): string
{
return __('admin.nav.settings');
}
public function getTitle(): string
{
return __('admin.pages.settings_title');
}
/** @var array<string, mixed>|null */
public ?array $data = [];
public function mount(
GeneralSettings $general,
SeoSettings $seo,
AiSettings $ai,
BlogSettings $blog,
CommentSettings $comment,
SnippetSettings $snippets,
): void {
$this->form->fill([
'site_name' => $general->site_name,
'site_url' => $general->site_url,
'site_description' => $general->site_description,
'active_theme' => $general->active_theme,
'attachments_url_prefix' => $general->attachments_url_prefix,
'default_content_format' => $general->default_content_format,
'import_content_format' => $general->import_content_format,
'import_convert_html_to_markdown' => $general->import_convert_html_to_markdown,
'posts_per_page' => $blog->posts_per_page,
'allow_comments' => $blog->allow_comments,
'comment_order' => $blog->comment_order,
'show_views' => $blog->show_views,
'show_author' => $blog->show_author,
'date_format' => $blog->date_format,
'close_comments_on_old_posts' => $blog->close_comments_on_old_posts,
'close_comments_days' => $blog->close_comments_days,
'guest_can_comment' => $comment->guest_can_comment,
'require_moderation' => $comment->require_moderation,
'rate_limit_per_minute' => $comment->rate_limit_per_minute,
'enable_website_field' => $comment->enable_website_field,
'forbidden_words' => $comment->forbidden_words,
'meta_title_suffix' => $seo->meta_title_suffix,
'default_description' => $seo->default_description,
'default_keywords' => $seo->default_keywords,
'json_ld_enabled' => $seo->json_ld_enabled,
'robots_index' => $seo->robots_index,
'twitter_site' => $seo->twitter_site,
'canonical_force_https' => $seo->canonical_force_https,
'ai_provider' => $ai->provider,
'ai_api_base_url' => $ai->api_base_url,
'ai_api_key' => $ai->api_key,
'ai_model' => $ai->model,
'comment_moderation_enabled' => $ai->comment_moderation_enabled,
'content_optimization_enabled' => $ai->content_optimization_enabled,
'analytics_head' => $snippets->analytics_head,
'body_end' => $snippets->body_end,
'ads_sidebar' => $snippets->ads_sidebar,
'ads_article_top' => $snippets->ads_article_top,
'ads_article_bottom' => $snippets->ads_article_bottom,
'header_banner' => $snippets->header_banner,
'custom_links_html' => $snippets->custom_links_html,
'footer_html' => $snippets->footer_html,
]);
}
public function defaultForm(Schema $schema): Schema
{
return $schema->statePath('data');
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make('settings')
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tab::make(__('admin.settings.tabs.site'))
->icon(Heroicon::OutlinedGlobeAlt)
->schema([
TextInput::make('site_name')->label(__('admin.settings.site_name'))->required(),
TextInput::make('site_url')->label(__('admin.settings.site_url'))->required()->url(),
Textarea::make('site_description')->label(__('admin.settings.site_description'))->rows(3),
TextInput::make('active_theme')->label(__('admin.settings.active_theme'))->required(),
TextInput::make('attachments_url_prefix')->label(__('admin.settings.attachments_url_prefix'))->required(),
Select::make('default_content_format')
->label(__('admin.settings.default_content_format'))
->options([
ContentFormat::MARKDOWN => __('admin.options.markdown'),
ContentFormat::HTML => __('admin.options.html'),
])->required(),
Select::make('import_content_format')
->label(__('admin.settings.import_content_format'))
->options([
ContentFormat::HTML => __('admin.options.html'),
ContentFormat::MARKDOWN => __('admin.options.markdown'),
])->required(),
Toggle::make('import_convert_html_to_markdown')
->label(__('admin.settings.import_convert_html_to_markdown')),
]),
Tab::make(__('admin.settings.tabs.reading'))
->icon(Heroicon::OutlinedNewspaper)
->schema([
TextInput::make('posts_per_page')->label(__('admin.settings.posts_per_page'))->numeric()->required()->minValue(1)->maxValue(100),
TextInput::make('date_format')->label(__('admin.settings.date_format'))->required(),
Toggle::make('show_views')->label(__('admin.settings.show_views')),
Toggle::make('show_author')->label(__('admin.settings.show_author')),
Toggle::make('allow_comments')->label(__('admin.settings.allow_comments')),
Select::make('comment_order')
->label(__('admin.settings.comment_order'))
->options([
'asc' => __('admin.options.comment_order_asc'),
'desc' => __('admin.options.comment_order_desc'),
])
->required(),
Toggle::make('close_comments_on_old_posts')->label(__('admin.settings.close_comments_on_old_posts')),
TextInput::make('close_comments_days')->label(__('admin.settings.close_comments_days'))->numeric()->minValue(1),
]),
Tab::make(__('admin.settings.tabs.comments'))
->icon(Heroicon::OutlinedChatBubbleLeftRight)
->schema([
Toggle::make('guest_can_comment')->label(__('admin.settings.guest_can_comment')),
Toggle::make('require_moderation')->label(__('admin.settings.require_moderation')),
TextInput::make('rate_limit_per_minute')->label(__('admin.settings.rate_limit_per_minute'))->numeric()->minValue(1),
Toggle::make('enable_website_field')->label(__('admin.settings.enable_website_field')),
Textarea::make('forbidden_words')->label(__('admin.settings.forbidden_words'))->rows(3),
]),
Tab::make(__('admin.settings.tabs.seo'))
->icon(Heroicon::OutlinedMagnifyingGlass)
->schema([
TextInput::make('meta_title_suffix')->label(__('admin.settings.meta_title_suffix')),
Textarea::make('default_description')->label(__('admin.settings.default_description'))->rows(3),
TextInput::make('default_keywords')->label(__('admin.settings.default_keywords')),
Toggle::make('json_ld_enabled')->label(__('admin.settings.json_ld_enabled')),
Toggle::make('robots_index')->label(__('admin.settings.robots_index')),
TextInput::make('twitter_site')->label(__('admin.settings.twitter_site')),
Toggle::make('canonical_force_https')->label(__('admin.settings.canonical_force_https')),
]),
Tab::make(__('admin.settings.tabs.storage'))
->icon(Heroicon::OutlinedCloudArrowUp)
->schema([
TextInput::make('attachments_url_prefix')
->label(__('admin.settings.attachments_legacy_prefix'))
->helperText(__('admin.helpers.attachments_prefix'))
->required(),
]),
Tab::make(__('admin.settings.tabs.ai'))
->icon(Heroicon::OutlinedSparkles)
->schema([
Select::make('ai_provider')->label(__('admin.settings.ai_provider'))->options([
'stub' => __('admin.options.ai_provider_stub'),
'openai_compatible' => __('admin.options.ai_provider_openai'),
])->required(),
TextInput::make('ai_api_base_url')->label(__('admin.settings.ai_api_base_url')),
TextInput::make('ai_api_key')->label(__('admin.settings.ai_api_key'))->password()->revealable(),
TextInput::make('ai_model')->label(__('admin.settings.ai_model')),
Toggle::make('comment_moderation_enabled')->label(__('admin.settings.comment_moderation_enabled')),
Toggle::make('content_optimization_enabled')->label(__('admin.settings.content_optimization_enabled')),
]),
Tab::make(__('admin.settings.tabs.snippets'))
->icon(Heroicon::OutlinedCodeBracket)
->schema([
Section::make(__('admin.settings.snippet_groups.analytics'))
->description(__('admin.settings.snippet_groups.analytics_help'))
->icon(Heroicon::OutlinedChartBar)
->collapsible()
->schema([
Textarea::make('analytics_head')
->label(__('admin.settings.analytics_head'))
->helperText(ThemeSlot::hint(ThemeSlot::HEAD).' '.__('admin.helpers.analytics_extra'))
->rows(5)
->columnSpanFull(),
Textarea::make('body_end')
->label(__('admin.settings.body_end'))
->helperText(ThemeSlot::hint(ThemeSlot::BODY_END))
->rows(3)
->columnSpanFull(),
]),
Section::make(__('admin.settings.snippet_groups.ads'))
->description(__('admin.settings.snippet_groups.ads_help'))
->icon(Heroicon::OutlinedMegaphone)
->collapsed()
->schema([
Textarea::make('header_banner')
->label(__('admin.settings.header_banner'))
->helperText(ThemeSlot::hint(ThemeSlot::HEADER_AFTER))
->rows(3)
->columnSpanFull(),
Textarea::make('ads_sidebar')
->label(__('admin.settings.ads_sidebar'))
->helperText(ThemeSlot::hint(ThemeSlot::SIDEBAR).' '.__('admin.helpers.ads_sidebar_extra'))
->rows(3)
->columnSpanFull(),
Textarea::make('ads_article_top')
->label(__('admin.settings.ads_article_top'))
->helperText(ThemeSlot::hint(ThemeSlot::ARTICLE_TOP))
->rows(3)
->columnSpanFull(),
Textarea::make('ads_article_bottom')
->label(__('admin.settings.ads_article_bottom'))
->helperText(ThemeSlot::hint(ThemeSlot::ARTICLE_BOTTOM))
->rows(3)
->columnSpanFull(),
]),
Section::make(__('admin.settings.snippet_groups.misc'))
->description(__('admin.settings.snippet_groups.misc_help'))
->icon(Heroicon::OutlinedLink)
->collapsed()
->schema([
Textarea::make('custom_links_html')
->label(__('admin.settings.custom_links_html'))
->helperText(ThemeSlot::hint(ThemeSlot::SIDEBAR_AFTER))
->rows(3)
->columnSpanFull(),
Textarea::make('footer_html')
->label(__('admin.settings.footer_html'))
->helperText(ThemeSlot::hint(ThemeSlot::FOOTER_BEFORE))
->rows(3)
->columnSpanFull(),
]),
]),
]),
]);
}
public function content(Schema $schema): Schema
{
return $schema->components([
Form::make([EmbeddedSchema::make('form')])
->id('form')
->livewireSubmitHandler('save')
->footer([
Actions::make([
Action::make('save')
->label(__('admin.actions.save'))
->submit('save'),
]),
]),
]);
}
public function save(
GeneralSettings $general,
SeoSettings $seo,
AiSettings $ai,
BlogSettings $blog,
CommentSettings $comment,
SnippetSettings $snippets,
): void {
$data = $this->form->getState();
$general->site_name = $data['site_name'];
$general->site_url = $data['site_url'];
$general->site_description = $data['site_description'] ?? null;
$general->active_theme = $data['active_theme'];
$general->attachments_url_prefix = $data['attachments_url_prefix'];
$general->default_content_format = $data['default_content_format'];
$general->import_content_format = $data['import_content_format'];
$general->import_convert_html_to_markdown = (bool) $data['import_convert_html_to_markdown'];
$general->save();
config(['larablog.attachments_url_prefix' => $general->attachments_url_prefix]);
$blog->posts_per_page = (int) $data['posts_per_page'];
$blog->allow_comments = (bool) $data['allow_comments'];
$blog->comment_order = $data['comment_order'];
$blog->show_views = (bool) $data['show_views'];
$blog->show_author = (bool) $data['show_author'];
$blog->date_format = $data['date_format'];
$blog->close_comments_on_old_posts = (bool) $data['close_comments_on_old_posts'];
$blog->close_comments_days = (int) $data['close_comments_days'];
$blog->save();
$comment->guest_can_comment = (bool) $data['guest_can_comment'];
$comment->require_moderation = (bool) $data['require_moderation'];
$comment->rate_limit_per_minute = (int) $data['rate_limit_per_minute'];
$comment->enable_website_field = (bool) $data['enable_website_field'];
$comment->forbidden_words = (string) ($data['forbidden_words'] ?? '');
$comment->save();
$seo->meta_title_suffix = $data['meta_title_suffix'] ?? null;
$seo->default_description = $data['default_description'] ?? null;
$seo->default_keywords = $data['default_keywords'] ?? null;
$seo->json_ld_enabled = (bool) $data['json_ld_enabled'];
$seo->robots_index = (bool) $data['robots_index'];
$seo->twitter_site = $data['twitter_site'] ?? null;
$seo->canonical_force_https = (bool) $data['canonical_force_https'];
$seo->save();
$ai->provider = $data['ai_provider'];
$ai->api_base_url = $data['ai_api_base_url'] ?? null;
$ai->api_key = $data['ai_api_key'] ?? null;
$ai->model = $data['ai_model'] ?? null;
$ai->comment_moderation_enabled = (bool) $data['comment_moderation_enabled'];
$ai->content_optimization_enabled = (bool) $data['content_optimization_enabled'];
$ai->save();
$snippets->analytics_head = (string) ($data['analytics_head'] ?? '');
$snippets->body_end = (string) ($data['body_end'] ?? '');
$snippets->ads_sidebar = (string) ($data['ads_sidebar'] ?? '');
$snippets->ads_article_top = (string) ($data['ads_article_top'] ?? '');
$snippets->ads_article_bottom = (string) ($data['ads_article_bottom'] ?? '');
$snippets->header_banner = (string) ($data['header_banner'] ?? '');
$snippets->custom_links_html = (string) ($data['custom_links_html'] ?? '');
$snippets->footer_html = (string) ($data['footer_html'] ?? '');
$snippets->save();
Notification::make()->title(__('admin.messages.settings_saved'))->success()->send();
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use BackedEnum;
use Filament\Support\Icons\Heroicon;
class ThemeMarketplacePage extends PluginSkeletonPage
{
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedSwatch;
protected static ?int $navigationSort = 104;
protected static function pluginName(): string
{
return 'larablog/theme-marketplace';
}
protected static function navKey(): string
{
return 'theme_marketplace';
}
}