front end fixes
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
@extends('stylist-theme-saasland::layouts.app')
|
||||
@php($title = 'Blog')
|
||||
@php($description = 'Ideas, guides and updates from our team.')
|
||||
@php($eyebrow = 'Insights')
|
||||
@php($breadcrumbs = ['Blog' => null])
|
||||
@section('content')
|
||||
<x-stylist-theme-saasland::layouts.app
|
||||
title="Blog"
|
||||
description="Ideas, guides and updates from our team."
|
||||
eyebrow="Insights"
|
||||
:breadcrumbs="['Blog' => null]"
|
||||
>
|
||||
<section class="content-section">
|
||||
<div class="wrap">
|
||||
<div class="content-grid">
|
||||
@@ -24,4 +24,4 @@
|
||||
<div class="theme-pagination">{{ $records->links() }}</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
</x-stylist-theme-saasland::layouts.app>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@extends('stylist-theme-saasland::layouts.app')
|
||||
@php($title = $record->title)
|
||||
@php($description = $record->excerpt)
|
||||
@php($eyebrow = 'Blog')
|
||||
@php($breadcrumbs = ['Blog' => route('blog.public.index'), $record->title => null])
|
||||
@section('content')
|
||||
<x-stylist-theme-saasland::layouts.app
|
||||
:title="$record->title"
|
||||
:description="$record->excerpt"
|
||||
eyebrow="Blog"
|
||||
:breadcrumbs="['Blog' => route('blog.public.index'), $record->title => null]"
|
||||
>
|
||||
<section class="content-section">
|
||||
<div class="wrap content-layout">
|
||||
<article class="content-card prose-content">
|
||||
@@ -16,4 +16,4 @@
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
</x-stylist-theme-saasland::layouts.app>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@extends('stylist-theme-saasland::layouts.app')
|
||||
@php($title = 'News')
|
||||
@php($description = 'The latest announcements, releases and company updates.')
|
||||
@php($eyebrow = 'Updates')
|
||||
@php($breadcrumbs = ['News' => null])
|
||||
@section('content')
|
||||
<x-stylist-theme-saasland::layouts.app
|
||||
title="News"
|
||||
description="The latest announcements, releases and company updates."
|
||||
eyebrow="Updates"
|
||||
:breadcrumbs="['News' => null]"
|
||||
>
|
||||
<section class="content-section">
|
||||
<div class="wrap">
|
||||
<div class="content-grid">
|
||||
@@ -24,4 +24,4 @@
|
||||
<div class="theme-pagination">{{ $records->links() }}</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
</x-stylist-theme-saasland::layouts.app>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@extends('stylist-theme-saasland::layouts.app')
|
||||
@php($title = $record->title)
|
||||
@php($description = $record->excerpt)
|
||||
@php($eyebrow = 'News')
|
||||
@php($breadcrumbs = ['News' => route('news.public.index'), $record->title => null])
|
||||
@section('content')
|
||||
<x-stylist-theme-saasland::layouts.app
|
||||
:title="$record->title"
|
||||
:description="$record->excerpt"
|
||||
eyebrow="News"
|
||||
:breadcrumbs="['News' => route('news.public.index'), $record->title => null]"
|
||||
>
|
||||
<section class="content-section">
|
||||
<div class="wrap content-layout">
|
||||
<article class="content-card prose-content">
|
||||
@@ -16,4 +16,4 @@
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
</x-stylist-theme-saasland::layouts.app>
|
||||
|
||||
@@ -32,6 +32,17 @@ final class NewsletterController
|
||||
return back()->with('status', 'Campaign created.');
|
||||
}
|
||||
|
||||
public function publicSubscribe(Request $r): RedirectResponse
|
||||
{
|
||||
$data = $r->validate(['email' => ['required', 'email', 'max:255'], 'name' => ['nullable', 'string', 'max:255']]);
|
||||
Subscriber::query()->updateOrCreate(
|
||||
['email' => strtolower($data['email'])],
|
||||
['name' => $data['name'] ?? null, 'status' => 'confirmed', 'confirmation_token' => Str::random(48), 'confirmed_at' => now()]
|
||||
);
|
||||
|
||||
return back()->with('status', 'Thank you for subscribing.');
|
||||
}
|
||||
|
||||
public function destroySubscriber(Subscriber $subscriber): RedirectResponse
|
||||
{
|
||||
$subscriber->delete();
|
||||
|
||||
@@ -9,3 +9,5 @@ Route::middleware(['auth', 'verified'])->prefix('administration/newsletter')->na
|
||||
Route::delete('/subscribers/{subscriber}', [NewsletterController::class, 'destroySubscriber'])->name('subscribers.destroy');
|
||||
Route::post('/campaigns', [NewsletterController::class, 'storeCampaign'])->name('campaigns.store');
|
||||
});
|
||||
|
||||
Route::post('/newsletter/subscribe', [NewsletterController::class, 'publicSubscribe'])->middleware('throttle:10,1')->name('newsletter.public.subscribe');
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@extends('stylist-theme-saasland::layouts.app')
|
||||
@php($title = $page->title)
|
||||
@php($description = $page->summary)
|
||||
@php($eyebrow = 'Page')
|
||||
@php($breadcrumbs = [$page->title => null])
|
||||
@section('content')
|
||||
<x-stylist-theme-saasland::layouts.app
|
||||
:title="$page->title"
|
||||
:description="$page->summary"
|
||||
eyebrow="Page"
|
||||
:breadcrumbs="[$page->title => null]"
|
||||
>
|
||||
<section class="content-section">
|
||||
<div class="wrap content-layout">
|
||||
<article class="content-card prose-content">
|
||||
@@ -15,4 +15,4 @@
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
</x-stylist-theme-saasland::layouts.app>
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table): void {
|
||||
$table->string('product_type')->default('product')->after('name');
|
||||
$table->text('description')->nullable()->after('product_type');
|
||||
$table->decimal('price', 12, 2)->nullable()->after('description');
|
||||
$table->string('currency', 3)->default('GBP')->after('price');
|
||||
$table->string('billing_period')->nullable()->after('currency');
|
||||
$table->json('features')->nullable()->after('billing_period');
|
||||
$table->string('cta_label')->nullable()->after('features');
|
||||
$table->string('cta_url')->nullable()->after('cta_label');
|
||||
$table->boolean('is_featured')->default(false)->after('cta_url');
|
||||
$table->unsignedInteger('sort_order')->default(0)->after('is_featured');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table): void {
|
||||
$table->dropColumn([
|
||||
'product_type', 'description', 'price', 'currency', 'billing_period',
|
||||
'features', 'cta_label', 'cta_url', 'is_featured', 'sort_order',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -11,8 +11,35 @@ final class StoreProductRequest extends FormRequest
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'is_active' => $this->boolean('is_active'),
|
||||
'is_featured' => $this->boolean('is_featured'),
|
||||
'features' => collect(preg_split('/\r\n|\r|\n/', (string) $this->input('features')))
|
||||
->map(fn (string $feature) => trim($feature))
|
||||
->filter()
|
||||
->values()
|
||||
->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return ['name' => ['required', 'string', 'max:255'], 'is_active' => ['sometimes', 'boolean']];
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'product_type' => ['required', 'in:product,plan'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'price' => ['nullable', 'numeric', 'min:0'],
|
||||
'currency' => ['required', 'string', 'size:3'],
|
||||
'billing_period' => ['nullable', 'string', 'max:40'],
|
||||
'features' => ['array'],
|
||||
'features.*' => ['string', 'max:255'],
|
||||
'cta_label' => ['nullable', 'string', 'max:80'],
|
||||
'cta_url' => ['nullable', 'string', 'max:2048'],
|
||||
'is_featured' => ['boolean'],
|
||||
'sort_order' => ['integer', 'min:0'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ final class Product extends Model
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['is_active' => 'boolean'];
|
||||
return [
|
||||
'price' => 'decimal:2',
|
||||
'features' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'is_featured' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,25 @@
|
||||
<form method="POST">@csrf<flux:input name="name" :label="__('Name')" required /><flux:button type="submit" variant="primary">{{ __('Save Product') }}</flux:button></form>
|
||||
<form method="POST" class="space-y-5">
|
||||
@csrf
|
||||
<div class="grid gap-5 md:grid-cols-2">
|
||||
<flux:input name="name" :label="__('Name')" required />
|
||||
<flux:select name="product_type" :label="__('Type')">
|
||||
<option value="product">Product</option>
|
||||
<option value="plan">Pricing plan</option>
|
||||
</flux:select>
|
||||
<flux:input name="price" type="number" step="0.01" min="0" :label="__('Price')" />
|
||||
<flux:input name="currency" value="GBP" maxlength="3" :label="__('Currency')" />
|
||||
<flux:input name="billing_period" value="month" :label="__('Billing period')" />
|
||||
<flux:input name="sort_order" type="number" min="0" value="0" :label="__('Sort order')" />
|
||||
</div>
|
||||
<flux:textarea name="description" :label="__('Description')" rows="3" />
|
||||
<flux:textarea name="features" :label="__('Features — one per line')" rows="6" />
|
||||
<div class="grid gap-5 md:grid-cols-2">
|
||||
<flux:input name="cta_label" :label="__('Button label')" />
|
||||
<flux:input name="cta_url" :label="__('Button URL')" />
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-6">
|
||||
<flux:checkbox name="is_active" value="1" checked :label="__('Active')" />
|
||||
<flux:checkbox name="is_featured" value="1" :label="__('Featured / most popular')" />
|
||||
</div>
|
||||
<flux:button type="submit" variant="primary">{{ __('Save Product') }}</flux:button>
|
||||
</form>
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
<x-layouts.app :title="__('Product')">
|
||||
<div class="space-y-6"><h1 class="text-2xl font-semibold">Product</h1>@foreach ($records as $record)<div>{{ $record->name }}</div>@endforeach{{ $records->links() }}</div>
|
||||
<x-layouts.app :title="__('Products & Pricing')">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Products & Pricing</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">Manage catalogue products and public-facing pricing plans from one place.</p>
|
||||
</div>
|
||||
<section class="rounded-2xl border border-zinc-200/70 bg-white/80 p-6 shadow-sm backdrop-blur dark:border-white/10 dark:bg-zinc-950/70">
|
||||
@include('products::admin.products.form')
|
||||
</section>
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
@forelse ($records as $record)
|
||||
<article class="rounded-2xl border border-zinc-200/70 bg-white/80 p-5 shadow-sm backdrop-blur dark:border-white/10 dark:bg-zinc-950/70">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div><h2 class="font-semibold">{{ $record->name }}</h2><p class="text-sm text-zinc-500">{{ ucfirst($record->product_type ?? 'product') }}</p></div>
|
||||
@if($record->is_featured)<span class="rounded-full bg-orange-500/10 px-2.5 py-1 text-xs font-semibold text-orange-600">Featured</span>@endif
|
||||
</div>
|
||||
@if($record->price !== null)<p class="mt-4 text-2xl font-bold">{{ $record->currency }} {{ $record->price }} <small class="text-sm font-normal">/{{ $record->billing_period }}</small></p>@else<p class="mt-4 text-2xl font-bold">Custom</p>@endif
|
||||
<p class="mt-2 text-sm text-zinc-500">{{ $record->description }}</p>
|
||||
</article>
|
||||
@empty
|
||||
<p class="text-sm text-zinc-500">No products or plans have been created.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
{{ $records->links() }}
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
|
||||
@@ -15,6 +15,7 @@ use Modules\Stylist\Html\ThemeHtmlBuilder;
|
||||
use Modules\Stylist\Html\ThemeSeoBuilder;
|
||||
use Modules\Stylist\Support\FrontendNavigation;
|
||||
use Modules\Stylist\Support\ModuleInfo;
|
||||
use Modules\Stylist\Services\FrontendContent;
|
||||
use Modules\Stylist\Services\ThemeSettings;
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
use Modules\Stylist\Theme\Stylist;
|
||||
@@ -71,6 +72,7 @@ final class StylistServiceProvider extends ServiceProvider
|
||||
$theme = $this->app->make(Loader::class)->find('saasland');
|
||||
$settings = $this->app->make(ThemeSettings::class)->get($theme);
|
||||
$navigation = $this->app->make(FrontendNavigation::class);
|
||||
$homepage = $this->app->make(FrontendContent::class)->homepage($settings);
|
||||
|
||||
$view->with([
|
||||
'siteTitle' => filled($settings['site_title'] ?? null) ? $settings['site_title'] : config('app.name', 'Saasland'),
|
||||
@@ -81,6 +83,8 @@ final class StylistServiceProvider extends ServiceProvider
|
||||
'linkedinUrl' => $settings['linkedin_url'] ?? null,
|
||||
'primaryNavigation' => $navigation->primary(),
|
||||
'footerNavigation' => $navigation->footer(),
|
||||
'themeSettings' => $settings,
|
||||
...$homepage,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Services;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\Blog\Models\Blog;
|
||||
use Modules\Faq\Models\Faq;
|
||||
use Modules\News\Models\News;
|
||||
use Modules\Products\Models\Product;
|
||||
use Modules\Team\Models\TeamMember;
|
||||
use Modules\Testimonials\Models\Testimonial;
|
||||
use Throwable;
|
||||
|
||||
final class FrontendContent
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
public function homepage(array $settings): array
|
||||
{
|
||||
return [
|
||||
'pricingPlans' => $this->enabled($settings, 'show_pricing', true) ? $this->pricingPlans() : collect(),
|
||||
'testimonials' => $this->enabled($settings, 'show_testimonials', true) ? $this->testimonials() : collect(),
|
||||
'faqs' => $this->enabled($settings, 'show_faq', true) ? $this->faqs() : collect(),
|
||||
'teamMembers' => $this->enabled($settings, 'show_team', false) ? $this->team() : collect(),
|
||||
'latestBlogPosts' => $this->enabled($settings, 'show_blog', true) ? $this->blogPosts() : collect(),
|
||||
'latestNewsArticles' => $this->enabled($settings, 'show_news', true) ? $this->newsArticles() : collect(),
|
||||
];
|
||||
}
|
||||
|
||||
private function pricingPlans(): Collection
|
||||
{
|
||||
return $this->query('products', fn () => Product::query()
|
||||
->where('is_active', true)
|
||||
->where('product_type', 'plan')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->limit(6)
|
||||
->get());
|
||||
}
|
||||
|
||||
private function testimonials(): Collection
|
||||
{
|
||||
return $this->query('testimonials', fn () => Testimonial::query()
|
||||
->where('enabled', true)
|
||||
->latest('id')
|
||||
->limit(6)
|
||||
->get());
|
||||
}
|
||||
|
||||
private function faqs(): Collection
|
||||
{
|
||||
return $this->query('faqs', fn () => Faq::query()
|
||||
->where('enabled', true)
|
||||
->orderBy('sort_order')
|
||||
->limit(12)
|
||||
->get());
|
||||
}
|
||||
|
||||
private function team(): Collection
|
||||
{
|
||||
return $this->query('team_members', fn () => TeamMember::query()
|
||||
->where('enabled', true)
|
||||
->orderBy('sort_order')
|
||||
->limit(8)
|
||||
->get());
|
||||
}
|
||||
|
||||
private function blogPosts(): Collection
|
||||
{
|
||||
return $this->query('blog_posts', fn () => Blog::query()
|
||||
->published()
|
||||
->latest('published_at')
|
||||
->limit(3)
|
||||
->get());
|
||||
}
|
||||
|
||||
private function newsArticles(): Collection
|
||||
{
|
||||
return $this->query('news_articles', fn () => News::query()
|
||||
->published()
|
||||
->latest('published_at')
|
||||
->limit(3)
|
||||
->get());
|
||||
}
|
||||
|
||||
private function query(string $table, callable $callback): Collection
|
||||
{
|
||||
try {
|
||||
return Schema::hasTable($table) ? $callback() : collect();
|
||||
} catch (Throwable) {
|
||||
return collect();
|
||||
}
|
||||
}
|
||||
|
||||
private function enabled(array $settings, string $key, bool $default): bool
|
||||
{
|
||||
return filter_var($settings[$key] ?? $default, FILTER_VALIDATE_BOOL);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user