phase oen updates
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
return [
|
||||
'default_page_key' => 'home',
|
||||
'regions' => ['header', 'body', 'footer'],
|
||||
];
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace Modules\Content\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\Content\Models\ContentDocument;
|
||||
use Modules\Content\Models\ContentType;
|
||||
use Modules\Content\Models\PageContent;
|
||||
|
||||
final class InstallSaaslandContent extends Command
|
||||
{
|
||||
protected $signature = 'content:install-saasland {--force : Replace the existing home composition}';
|
||||
protected $description = 'Install editable Saasland header, body and footer content documents.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
DB::transaction(function (): void {
|
||||
if ($this->option('force')) PageContent::query()->where('page_key','home')->delete();
|
||||
$definitions = $this->definitions();
|
||||
$order = ['site-header'=>['header',10], 'hero'=>['body',10], 'feature-grid'=>['body',20], 'product-feed'=>['body',30], 'testimonial-feed'=>['body',40], 'team-feed'=>['body',50], 'blog-feed'=>['body',60], 'news-feed'=>['body',70], 'faq-feed'=>['body',80], 'newsletter-form'=>['body',90], 'call-to-action'=>['body',100], 'site-footer'=>['footer',10]];
|
||||
foreach ($definitions as $key => $definition) {
|
||||
$type = ContentType::query()->updateOrCreate(['key'=>$key], $definition['type']);
|
||||
$document = ContentDocument::query()->updateOrCreate(['slug'=>'saasland-'.$key], ['content_type_id'=>$type->id,'title'=>$definition['title'],'data'=>$definition['data'],'status'=>'published','published_at'=>now()]);
|
||||
[$region,$sort] = $order[$key];
|
||||
PageContent::query()->updateOrCreate(['page_key'=>'home','region'=>$region,'sort_order'=>$sort], ['content_document_id'=>$document->id,'is_enabled'=>true]);
|
||||
}
|
||||
});
|
||||
$this->components->info('Saasland content composition installed.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function definitions(): array
|
||||
{
|
||||
$type = fn(string $name,string $renderer,array $regions) => ['name'=>$name,'renderer'=>$renderer,'allowed_regions'=>$regions,'is_system'=>true,'is_active'=>true,'schema'=>[]];
|
||||
return [
|
||||
'site-header'=>['title'=>'Main website header','type'=>$type('Site header','site-header',['header']),'data'=>['show_login'=>true,'show_registration'=>true,'sticky'=>true]],
|
||||
'hero'=>['title'=>'Homepage hero','type'=>$type('Hero','hero',['body']),'data'=>['eyebrow'=>'A better way to run your business','heading'=>'Transform Your Business With Our SaaS Solution','description'=>'Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams.','primary_label'=>'Get started free','primary_url'=>'/register','secondary_label'=>'Book demo','secondary_url'=>'#contact','show_metrics'=>true]],
|
||||
'feature-grid'=>['title'=>'Core features','type'=>$type('Feature grid','feature-grid',['body']),'data'=>['eyebrow'=>'Powerful features','heading'=>'Everything your team needs to grow','description'=>'Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.','items'=>[['title'=>'Smart automation','description'=>'Remove repetitive work and delays.'],['title'=>'Real-time analytics','description'=>'Turn activity into useful business insight.'],['title'=>'Content operations','description'=>'Create, review and publish confidently.'],['title'=>'Secure by design','description'=>'Protect users and data with dependable controls.'],['title'=>'Flexible integrations','description'=>'Connect the tools your organisation already uses.'],['title'=>'Modular architecture','description'=>'Activate only the capabilities each site needs.']]]],
|
||||
'product-feed'=>['title'=>'Pricing plans','type'=>$type('Product feed','product-feed',['body']),'data'=>['eyebrow'=>'Simple pricing','heading'=>'Choose a plan that fits','description'=>'Plans are managed from Products & Pricing in the CMS.','limit'=>6]],
|
||||
'testimonial-feed'=>['title'=>'Customer stories','type'=>$type('Testimonial feed','testimonial-feed',['body']),'data'=>['eyebrow'=>'Customer stories','heading'=>'Trusted by ambitious teams','limit'=>6]],
|
||||
'team-feed'=>['title'=>'Team','type'=>$type('Team feed','team-feed',['body']),'data'=>['eyebrow'=>'Our team','heading'=>'People behind the platform','limit'=>8]],
|
||||
'blog-feed'=>['title'=>'Latest blog posts','type'=>$type('Blog feed','blog-feed',['body']),'data'=>['eyebrow'=>'Insights','heading'=>'Latest from the blog','limit'=>3]],
|
||||
'news-feed'=>['title'=>'Latest news','type'=>$type('News feed','news-feed',['body']),'data'=>['eyebrow'=>'Newsroom','heading'=>'Latest news','limit'=>3]],
|
||||
'faq-feed'=>['title'=>'Frequently asked questions','type'=>$type('FAQ feed','faq-feed',['body']),'data'=>['eyebrow'=>'FAQ','heading'=>'Answers before you begin','limit'=>12]],
|
||||
'newsletter-form'=>['title'=>'Newsletter signup','type'=>$type('Newsletter form','newsletter-form',['body']),'data'=>['eyebrow'=>'Newsletter','heading'=>'Useful updates, no noise','description'=>'Receive product news, publishing guidance, and practical platform updates.','button_label'=>'Subscribe']],
|
||||
'call-to-action'=>['title'=>'Homepage final call to action','type'=>$type('Call to action','call-to-action',['body']),'data'=>['heading'=>'Ready to transform your business?','description'=>'Start building a faster, more connected operation today.','button_label'=>'Get started free','button_url'=>'/register']],
|
||||
'site-footer'=>['title'=>'Main website footer','type'=>$type('Site footer','site-footer',['footer']),'data'=>['description'=>'One connected platform for modern teams.','show_social_links'=>true,'show_back_to_top'=>true]],
|
||||
];
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?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::create('content_types', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('key')->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->json('schema')->nullable();
|
||||
$table->string('renderer');
|
||||
$table->json('allowed_regions')->nullable();
|
||||
$table->boolean('is_system')->default(false);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('content_documents', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('content_type_id')->constrained('content_types')->cascadeOnDelete();
|
||||
$table->string('title');
|
||||
$table->string('slug')->unique();
|
||||
$table->json('data')->nullable();
|
||||
$table->string('status')->default('draft')->index();
|
||||
$table->timestamp('published_at')->nullable()->index();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->string('language', 12)->nullable();
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::create('page_content', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('page_id')->nullable()->constrained('pages')->cascadeOnDelete();
|
||||
$table->string('page_key')->nullable()->index();
|
||||
$table->foreignId('content_document_id')->constrained('content_documents')->cascadeOnDelete();
|
||||
$table->string('region')->index();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->boolean('is_enabled')->default(true);
|
||||
$table->json('visibility_rules')->nullable();
|
||||
$table->json('presentation')->nullable();
|
||||
$table->timestamp('starts_at')->nullable();
|
||||
$table->timestamp('ends_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['page_key', 'region', 'sort_order'], 'page_content_key_region_order_unique');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('page_content');
|
||||
Schema::dropIfExists('content_documents');
|
||||
Schema::dropIfExists('content_types');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace Modules\Content\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Content\Models\ContentDocument;
|
||||
|
||||
final class ContentDocumentController extends Controller
|
||||
{
|
||||
public function index(): View { return view('content::admin.index',['documents'=>ContentDocument::query()->with('type')->withCount('placements')->orderBy('title')->paginate(30)]); }
|
||||
public function edit(ContentDocument $document): View { $document->load('type','placements'); return view('content::admin.edit',['document'=>$document]); }
|
||||
public function update(Request $request, ContentDocument $document): RedirectResponse
|
||||
{
|
||||
$validated=$request->validate(['title'=>['required','string','max:255'],'status'=>['required','in:draft,published'],'data_json'=>['required','json']]);
|
||||
$document->update(['title'=>$validated['title'],'status'=>$validated['status'],'published_at'=>$validated['status']==='published'?($document->published_at??now()):null,'data'=>json_decode($validated['data_json'],true,512,JSON_THROW_ON_ERROR),'updated_by'=>$request->user()?->id]);
|
||||
return back()->with('status','Content document updated.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace Modules\Content\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Content\Models\PageContent;
|
||||
|
||||
final class PageCompositionController extends Controller
|
||||
{
|
||||
public function edit(string $pageKey): View { return view('content::admin.composition',['pageKey'=>$pageKey,'placements'=>PageContent::query()->with('document.type')->where('page_key',$pageKey)->orderByRaw("CASE region WHEN 'header' THEN 1 WHEN 'body' THEN 2 ELSE 3 END")->orderBy('sort_order')->get()->groupBy('region')]); }
|
||||
public function update(Request $request, PageContent $placement): RedirectResponse
|
||||
{
|
||||
$validated=$request->validate(['sort_order'=>['required','integer','min:0'],'is_enabled'=>['nullable','boolean']]);
|
||||
$placement->update(['sort_order'=>$validated['sort_order'],'is_enabled'=>$request->boolean('is_enabled')]);
|
||||
return back()->with('status','Page placement updated.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace Modules\Content\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
final class ContentDocument extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
protected $fillable = ['content_type_id','title','slug','data','status','published_at','expires_at','language','created_by','updated_by'];
|
||||
protected function casts(): array { return ['data'=>'array','published_at'=>'datetime','expires_at'=>'datetime']; }
|
||||
public function type(): BelongsTo { return $this->belongsTo(ContentType::class, 'content_type_id'); }
|
||||
public function placements(): HasMany { return $this->hasMany(PageContent::class); }
|
||||
public function scopePublished($query) { return $query->where('status','published')->where(fn($q) => $q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at','>',now())); }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Content\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
final class ContentType extends Model
|
||||
{
|
||||
protected $fillable = ['name','key','description','schema','renderer','allowed_regions','is_system','is_active'];
|
||||
protected function casts(): array { return ['schema'=>'array','allowed_regions'=>'array','is_system'=>'boolean','is_active'=>'boolean']; }
|
||||
public function documents(): HasMany { return $this->hasMany(ContentDocument::class); }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace Modules\Content\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Pages\Models\Page;
|
||||
|
||||
final class PageContent extends Model
|
||||
{
|
||||
protected $table = 'page_content';
|
||||
protected $fillable = ['page_id','page_key','content_document_id','region','sort_order','is_enabled','visibility_rules','presentation','starts_at','ends_at'];
|
||||
protected function casts(): array { return ['is_enabled'=>'boolean','visibility_rules'=>'array','presentation'=>'array','starts_at'=>'datetime','ends_at'=>'datetime']; }
|
||||
public function document(): BelongsTo { return $this->belongsTo(ContentDocument::class, 'content_document_id'); }
|
||||
public function page(): BelongsTo { return $this->belongsTo(Page::class); }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace Modules\Content\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Content\Console\InstallSaaslandContent;
|
||||
use Modules\Content\Services\PageRenderer;
|
||||
|
||||
final class ContentServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->mergeConfigFrom(module_path('Content','Config/config.php'),'content');
|
||||
$this->app->singleton(PageRenderer::class);
|
||||
}
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('Content','Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('Content','Resources/views'),'content');
|
||||
$this->loadRoutesFrom(module_path('Content','Routes/web.php'));
|
||||
if ($this->app->runningInConsole()) $this->commands([InstallSaaslandContent::class]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# Content composition module
|
||||
|
||||
This module separates public pages into reusable typed content documents placed in named regions.
|
||||
|
||||
## Install the baseline composition
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
php artisan content:install-saasland
|
||||
```
|
||||
|
||||
Use `--force` to replace the existing `home` placements while retaining reusable documents.
|
||||
|
||||
```bash
|
||||
php artisan content:install-saasland --force
|
||||
```
|
||||
|
||||
## Administration
|
||||
|
||||
After signing in, open:
|
||||
|
||||
- `/admin/content` for reusable content documents.
|
||||
- `/admin/content/pages/home` for the homepage header/body/footer composition.
|
||||
|
||||
Documents contain structured JSON data and can be shared by several page placements. Placements own ordering, enabled state, scheduling, visibility and presentation metadata. The Saasland frontend falls back to its original hard-coded homepage until the migration and installer command have been run.
|
||||
|
||||
## Theme renderers
|
||||
|
||||
Saasland renderers are stored under:
|
||||
|
||||
`resources/themes/saasland/views/documents`
|
||||
|
||||
The renderer name is defined by each `content_types.renderer` value. Missing renderers fail safely through `content::documents.unsupported`.
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="'Page composition: '.$pageKey"><div class="p-6 max-w-5xl space-y-6"><h1 class="text-2xl font-semibold">Page composition: {{ $pageKey }}</h1>@if(session('status'))<p>{{ session('status') }}</p>@endif @foreach(['header','body','footer'] as $region)<section><h2 class="mb-2 text-lg font-semibold uppercase">{{ $region }}</h2><div class="space-y-2">@forelse($placements->get($region,collect()) as $placement)<form method="POST" action="{{ route('content.placements.update',$placement) }}" class="flex items-center gap-3 rounded-xl border p-3">@csrf @method('PUT')<div class="flex-1"><b>{{ $placement->document?->title }}</b><div class="text-xs text-zinc-500">{{ $placement->document?->type?->name }}</div></div><input class="w-20 rounded border p-2" type="number" name="sort_order" value="{{ $placement->sort_order }}"><label><input type="checkbox" name="is_enabled" value="1" @checked($placement->is_enabled)> Enabled</label><a class="underline" href="{{ route('content.documents.edit',$placement->document) }}">Edit</a><button class="rounded border px-3 py-2">Update</button></form>@empty<p class="text-sm text-zinc-500">No documents assigned.</p>@endforelse</div></section>@endforeach</div></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="$document->title"><form method="POST" action="{{ route('content.documents.update',$document) }}" class="p-6 max-w-5xl space-y-5">@csrf @method('PUT')<h1 class="text-2xl font-semibold">{{ $document->title }}</h1>@if(session('status'))<p>{{ session('status') }}</p>@endif<label class="block">Title<input class="mt-1 w-full rounded border p-2" name="title" value="{{ old('title',$document->title) }}"></label><label class="block">Status<select class="mt-1 w-full rounded border p-2" name="status"><option @selected($document->status==='draft')>draft</option><option @selected($document->status==='published')>published</option></select></label><label class="block">Structured data (JSON)<textarea class="mt-1 min-h-96 w-full rounded border p-3 font-mono text-sm" name="data_json">{{ old('data_json',json_encode($document->data,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)) }}</textarea></label>@error('data_json')<p class="text-red-600">{{ $message }}</p>@enderror<button class="rounded bg-black px-4 py-2 text-white">Save document</button></form></x-layouts.app>
|
||||
@@ -0,0 +1,5 @@
|
||||
<x-layouts.app :title="__('Content documents')">
|
||||
<div class="p-6 space-y-6"><div><h1 class="text-2xl font-semibold">Content documents</h1><p class="text-sm text-zinc-500">Reusable typed documents used by page regions.</p></div>
|
||||
<div><a class="underline" href="{{ route('content.pages.edit','home') }}">Edit homepage composition</a></div>
|
||||
<div class="overflow-x-auto rounded-xl border"><table class="w-full text-sm"><thead><tr><th class="p-3 text-left">Document</th><th>Type</th><th>Status</th><th>Uses</th><th></th></tr></thead><tbody>@foreach($documents as $document)<tr class="border-t"><td class="p-3">{{ $document->title }}</td><td>{{ $document->type?->name }}</td><td>{{ ucfirst($document->status) }}</td><td>{{ $document->placements_count }}</td><td><a class="underline" href="{{ route('content.documents.edit',$document) }}">Edit</a></td></tr>@endforeach</tbody></table></div>{{ $documents->links() }}</div>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,3 @@
|
||||
@if(app()->hasDebugModeEnabled())
|
||||
<!-- Unsupported content renderer: {{ $document->type?->renderer }} -->
|
||||
@endif
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Content\Http\Controllers\ContentDocumentController;
|
||||
use Modules\Content\Http\Controllers\PageCompositionController;
|
||||
|
||||
Route::middleware(['web','auth','verified'])->prefix('admin/content')->name('content.')->group(function (): void {
|
||||
Route::get('/', [ContentDocumentController::class,'index'])->name('index');
|
||||
Route::get('/documents/{document}/edit', [ContentDocumentController::class,'edit'])->name('documents.edit');
|
||||
Route::put('/documents/{document}', [ContentDocumentController::class,'update'])->name('documents.update');
|
||||
Route::get('/pages/{pageKey}', [PageCompositionController::class,'edit'])->name('pages.edit');
|
||||
Route::put('/placements/{placement}', [PageCompositionController::class,'update'])->name('placements.update');
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace Modules\Content\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Modules\Content\Models\PageContent;
|
||||
use Throwable;
|
||||
|
||||
final class PageRenderer
|
||||
{
|
||||
public function hasComposition(string $pageKey): bool
|
||||
{
|
||||
try { return Schema::hasTable('page_content') && PageContent::query()->where('page_key',$pageKey)->where('is_enabled',true)->exists(); }
|
||||
catch (Throwable) { return false; }
|
||||
}
|
||||
|
||||
public function renderRegion(string $pageKey, string $region): string
|
||||
{
|
||||
if (! $this->hasComposition($pageKey)) return '';
|
||||
|
||||
$placements = PageContent::query()
|
||||
->with('document.type')
|
||||
->where('page_key', $pageKey)
|
||||
->where('region', $region)
|
||||
->where('is_enabled', true)
|
||||
->where(fn($q) => $q->whereNull('starts_at')->orWhere('starts_at','<=',now()))
|
||||
->where(fn($q) => $q->whereNull('ends_at')->orWhere('ends_at','>',now()))
|
||||
->orderBy('sort_order')->get();
|
||||
|
||||
return $placements->map(function (PageContent $placement): string {
|
||||
$document = $placement->document;
|
||||
if (! $document || ! $document->type || $document->status !== 'published') return '';
|
||||
$view = 'stylist-theme-saasland::documents.'.$document->type->renderer;
|
||||
if (! View::exists($view)) $view = 'content::documents.unsupported';
|
||||
return View::make($view, ['document'=>$document,'data'=>$document->data ?? [],'placement'=>$placement])->render();
|
||||
})->implode("\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Content",
|
||||
"alias": "content",
|
||||
"version": "1.0.0",
|
||||
"description": "Composable typed content documents and page regions.",
|
||||
"keywords": ["cms", "content", "regions", "composition"],
|
||||
"priority": 22,
|
||||
"providers": ["Modules\\Content\\Providers\\ContentServiceProvider"],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": ["Core", "Pages", "Stylist"]
|
||||
}
|
||||
@@ -17,13 +17,16 @@ final class FrontendContent
|
||||
/** @return array<string, mixed> */
|
||||
public function homepage(array $settings): array
|
||||
{
|
||||
// Always provide source collections. Visibility is owned by page placements
|
||||
// in the composable content system; the legacy theme still applies its
|
||||
// existing show_* settings while no composition has been installed.
|
||||
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(),
|
||||
'pricingPlans' => $this->pricingPlans(),
|
||||
'testimonials' => $this->testimonials(),
|
||||
'faqs' => $this->faqs(),
|
||||
'teamMembers' => $this->team(),
|
||||
'latestBlogPosts' => $this->blogPosts(),
|
||||
'latestNewsArticles' => $this->newsArticles(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -45,5 +45,6 @@
|
||||
"Analytics": true,
|
||||
"Builder": true,
|
||||
"Marketplace": true,
|
||||
"CloudDevOps": true
|
||||
"CloudDevOps": true,
|
||||
"Content": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
@if($latestBlogPosts->isNotEmpty())<section class="section"><div class="wrap"><div class="heading"><span>{{ $data['eyebrow'] ?? '' }}</span><h2>{{ $data['heading'] ?? '' }}</h2></div><div class="grid three">@foreach($latestBlogPosts->take($data['limit'] ?? 3) as $post)<article class="card"><b>{{ optional($post->published_at)->format('d M Y') }}</b><h3>{{ $post->title }}</h3><p>{{ $post->excerpt }}</p><a class="read-more" href="{{ route('blog.public.show',$post->slug) }}">Read article →</a></article>@endforeach</div></div></section>@endif
|
||||
@@ -0,0 +1 @@
|
||||
<section class="cta" id="contact"><div class="wrap"><h2>{{ $data['heading'] ?? '' }}</h2><p>{{ $data['description'] ?? '' }}</p><a class="btn light" href="{{ $data['button_url'] ?? '#' }}">{{ $data['button_label'] ?? 'Get started' }} →</a></div></section>
|
||||
@@ -0,0 +1 @@
|
||||
@if($faqs->isNotEmpty())<section class="section"><div class="wrap faq"><div><span class="section-label">{{ $data['eyebrow'] ?? 'FAQ' }}</span><h2>{{ $data['heading'] ?? '' }}</h2></div><div>@foreach($faqs->take($data['limit'] ?? 12) as $faq)<article><button type="button" @click="openFaq = openFaq === {{ $faq->id }} ? null : {{ $faq->id }}">{{ $faq->question }}<span>+</span></button><p x-cloak x-show="openFaq === {{ $faq->id }}">{{ $faq->answer }}</p></article>@endforeach</div></div></section>@endif
|
||||
@@ -0,0 +1 @@
|
||||
<section class="section" id="features"><div class="wrap"><div class="heading"><span>{{ $data['eyebrow'] ?? '' }}</span><h2>{{ $data['heading'] ?? '' }}</h2><p>{{ $data['description'] ?? '' }}</p></div><div class="grid three">@foreach($data['items'] ?? [] as $i => $item)<article class="card"><b>{{ str_pad((string)($i+1),2,'0',STR_PAD_LEFT) }}</b><h3>{{ $item['title'] ?? '' }}</h3><p>{{ $item['description'] ?? '' }}</p></article>@endforeach</div></div></section>
|
||||
@@ -0,0 +1,5 @@
|
||||
<section class="hero"><div class="glow one"></div><div class="glow two"></div><div class="wrap hero-inner">
|
||||
<span class="eyebrow">{{ $data['eyebrow'] ?? '' }}</span><h1>{{ $data['heading'] ?? $document->title }}</h1><p>{{ $data['description'] ?? '' }}</p>
|
||||
<div class="actions"><a class="btn primary" href="{{ $data['primary_url'] ?? '#' }}">{{ $data['primary_label'] ?? 'Get started' }} →</a>@if(filled($data['secondary_label'] ?? null))<a class="btn secondary" href="{{ $data['secondary_url'] ?? '#' }}">{{ $data['secondary_label'] }}</a>@endif</div>
|
||||
@if($data['show_metrics'] ?? false)<div class="dashboard"><div class="dash-top"><b>Performance overview</b><span>Live</span></div><div class="metrics"><article><small>Monthly revenue</small><strong>£48,240</strong><em>+18.4%</em></article><article><small>Active users</small><strong>12,842</strong><em>+9.2%</em></article><article><small>Conversion</small><strong>7.8%</strong><em>+2.1%</em></article></div><div class="chart"><i style="height:28%"></i><i style="height:42%"></i><i style="height:35%"></i><i style="height:62%"></i><i style="height:54%"></i><i style="height:78%"></i><i style="height:92%"></i></div></div>@endif
|
||||
</div></section>
|
||||
@@ -0,0 +1 @@
|
||||
@if($latestNewsArticles->isNotEmpty())<section class="section alt"><div class="wrap"><div class="heading"><span>{{ $data['eyebrow'] ?? '' }}</span><h2>{{ $data['heading'] ?? '' }}</h2></div><div class="grid three">@foreach($latestNewsArticles->take($data['limit'] ?? 3) as $article)<article class="card"><b>{{ optional($article->published_at)->format('d M Y') }}</b><h3>{{ $article->title }}</h3><p>{{ $article->excerpt }}</p><a class="read-more" href="{{ route('news.public.show',$article->slug) }}">Read update →</a></article>@endforeach</div></div></section>@endif
|
||||
@@ -0,0 +1 @@
|
||||
<section class="section alt"><div class="wrap"><div class="content-card newsletter-panel"><div><span class="section-label">{{ $data['eyebrow'] ?? '' }}</span><h2>{{ $data['heading'] ?? '' }}</h2><p>{{ $data['description'] ?? '' }}</p></div><form method="POST" action="{{ route('newsletter.public.subscribe') }}">@csrf<input type="email" name="email" required placeholder="you@example.com" aria-label="Email address"><button class="btn primary" type="submit">{{ $data['button_label'] ?? 'Subscribe' }}</button></form></div></div></section>
|
||||
@@ -0,0 +1 @@
|
||||
@if($pricingPlans->isNotEmpty())<section class="section alt" id="pricing"><div class="wrap"><div class="heading"><span>{{ $data['eyebrow'] ?? '' }}</span><h2>{{ $data['heading'] ?? '' }}</h2><p>{{ $data['description'] ?? '' }}</p></div><div class="grid three pricing">@foreach($pricingPlans->take($data['limit'] ?? 6) as $plan)<article class="card plan {{ $plan->is_featured ? 'featured' : '' }}">@if($plan->is_featured)<div class="popular">Most popular</div>@endif<h3>{{ $plan->name }}</h3><strong>@if($plan->price !== null){{ $plan->currency === 'GBP' ? '£' : $plan->currency.' ' }}{{ rtrim(rtrim(number_format((float)$plan->price,2), '0'), '.') }}<small>/{{ $plan->billing_period ?: 'month' }}</small>@else Custom @endif</strong><p>{{ $plan->description }}</p><ul>@foreach((array)$plan->features as $feature)<li>{{ $feature }}</li>@endforeach</ul><a class="btn {{ $plan->is_featured ? 'primary' : 'secondary' }}" href="{{ $plan->cta_url ?: '#contact' }}">{{ $plan->cta_label ?: 'Learn more' }}</a></article>@endforeach</div></div></section>@endif
|
||||
@@ -0,0 +1 @@
|
||||
@include('stylist-theme-saasland::partials.footer')
|
||||
@@ -0,0 +1 @@
|
||||
@include('stylist-theme-saasland::partials.header')
|
||||
@@ -0,0 +1 @@
|
||||
@if($teamMembers->isNotEmpty())<section class="section alt"><div class="wrap"><div class="heading"><span>{{ $data['eyebrow'] ?? '' }}</span><h2>{{ $data['heading'] ?? '' }}</h2></div><div class="grid three">@foreach($teamMembers->take($data['limit'] ?? 8) as $member)<article class="card"><h3>{{ $member->name }}</h3><b>{{ $member->job_title }}</b><p>{{ $member->biography }}</p></article>@endforeach</div></div></section>@endif
|
||||
@@ -0,0 +1 @@
|
||||
@if($testimonials->isNotEmpty())<section class="section"><div class="wrap"><div class="heading"><span>{{ $data['eyebrow'] ?? '' }}</span><h2>{{ $data['heading'] ?? '' }}</h2></div><div class="grid three">@foreach($testimonials->take($data['limit'] ?? 6) as $testimonial)<blockquote class="card">“{{ $testimonial->content }}”<footer>{{ $testimonial->name }}@if($testimonial->company) · {{ $testimonial->company }}@endif</footer></blockquote>@endforeach</div></div></section>@endif
|
||||
@@ -4,71 +4,15 @@
|
||||
$title = $siteTitle;
|
||||
$description = $siteDescription;
|
||||
$hero = true;
|
||||
$enabled = fn (string $key, bool $default = true) => filter_var($themeSettings[$key] ?? $default, FILTER_VALIDATE_BOOL);
|
||||
$heroTitle = filled($themeSettings['hero_title'] ?? null) ? $themeSettings['hero_title'] : 'Transform Your Business With Our SaaS Solution';
|
||||
$heroText = filled($themeSettings['hero_description'] ?? null) ? $themeSettings['hero_description'] : 'Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams.';
|
||||
$pageKey = 'home';
|
||||
$contentRenderer = app(\Modules\Content\Services\PageRenderer::class);
|
||||
$hasComposition = $contentRenderer->hasComposition($pageKey);
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
<section class="hero">
|
||||
<div class="glow one"></div><div class="glow two"></div>
|
||||
<div class="wrap hero-inner">
|
||||
<span class="eyebrow">{{ $themeSettings['hero_eyebrow'] ?? 'A better way to run your business' }}</span>
|
||||
<h1>{{ $heroTitle }}</h1>
|
||||
<p>{{ $heroText }}</p>
|
||||
<div class="actions">
|
||||
<a class="btn primary" href="{{ $themeSettings['primary_cta_url'] ?? (Route::has('register') ? route('register') : '#contact') }}">{{ $themeSettings['primary_cta_label'] ?? 'Get started free' }} →</a>
|
||||
<a class="btn secondary" href="{{ $themeSettings['secondary_cta_url'] ?? '#contact' }}">{{ $themeSettings['secondary_cta_label'] ?? 'Book demo' }}</a>
|
||||
</div>
|
||||
@if($enabled('show_metrics', true))
|
||||
<div class="dashboard">
|
||||
<div class="dash-top"><b>Performance overview</b><span>Live</span></div>
|
||||
<div class="metrics"><article><small>Monthly revenue</small><strong>£48,240</strong><em>+18.4%</em></article><article><small>Active users</small><strong>12,842</strong><em>+9.2%</em></article><article><small>Conversion</small><strong>7.8%</strong><em>+2.1%</em></article></div>
|
||||
<div class="chart" aria-label="Performance chart"><i style="height:28%"></i><i style="height:42%"></i><i style="height:35%"></i><i style="height:62%"></i><i style="height:54%"></i><i style="height:78%"></i><i style="height:92%"></i></div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if($enabled('show_features', true))
|
||||
<section class="section" id="features"><div class="wrap"><div class="heading"><span>Powerful features</span><h2>Everything your team needs to grow</h2><p>Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.</p></div><div class="grid three">
|
||||
@foreach([['Smart automation','Remove repetitive work and delays.'],['Real-time analytics','Turn activity into useful business insight.'],['Content operations','Create, review and publish confidently.'],['Secure by design','Protect users and data with dependable controls.'],['Flexible integrations','Connect the tools your organisation already uses.'],['Modular architecture','Activate only the capabilities each site needs.']] as $i => $feature)
|
||||
<article class="card"><b>{{ str_pad((string)($i+1),2,'0',STR_PAD_LEFT) }}</b><h3>{{ $feature[0] }}</h3><p>{{ $feature[1] }}</p></article>
|
||||
@endforeach
|
||||
</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_pricing', true) && $pricingPlans->isNotEmpty())
|
||||
<section class="section alt" id="pricing"><div class="wrap"><div class="heading"><span>Simple pricing</span><h2>Choose a plan that fits</h2><p>Plans are managed from Products & Pricing in the CMS.</p></div><div class="grid three pricing">
|
||||
@foreach($pricingPlans as $plan)
|
||||
<article class="card plan {{ $plan->is_featured ? 'featured' : '' }}">@if($plan->is_featured)<div class="popular">Most popular</div>@endif<h3>{{ $plan->name }}</h3><strong>@if($plan->price !== null){{ $plan->currency === 'GBP' ? '£' : $plan->currency.' ' }}{{ rtrim(rtrim(number_format((float)$plan->price,2), '0'), '.') }}<small>/{{ $plan->billing_period ?: 'month' }}</small>@else Custom @endif</strong><p>{{ $plan->description }}</p><ul>@foreach((array)$plan->features as $feature)<li>{{ $feature }}</li>@endforeach</ul><a class="btn {{ $plan->is_featured ? 'primary' : 'secondary' }}" href="{{ $plan->cta_url ?: '#contact' }}">{{ $plan->cta_label ?: 'Learn more' }}</a></article>
|
||||
@endforeach
|
||||
</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_testimonials', true) && $testimonials->isNotEmpty())
|
||||
<section class="section"><div class="wrap"><div class="heading"><span>Customer stories</span><h2>Trusted by ambitious teams</h2></div><div class="grid three">@foreach($testimonials as $testimonial)<blockquote class="card">“{{ $testimonial->content }}”<footer>{{ $testimonial->name }}@if($testimonial->company) · {{ $testimonial->company }}@endif</footer></blockquote>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_team', false) && $teamMembers->isNotEmpty())
|
||||
<section class="section alt"><div class="wrap"><div class="heading"><span>Our team</span><h2>People behind the platform</h2></div><div class="grid three">@foreach($teamMembers as $member)<article class="card"><h3>{{ $member->name }}</h3><b>{{ $member->job_title }}</b><p>{{ $member->biography }}</p></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_blog', true) && $latestBlogPosts->isNotEmpty())
|
||||
<section class="section"><div class="wrap"><div class="heading"><span>Insights</span><h2>Latest from the blog</h2></div><div class="grid three">@foreach($latestBlogPosts as $post)<article class="card"><b>{{ optional($post->published_at)->format('d M Y') }}</b><h3>{{ $post->title }}</h3><p>{{ $post->excerpt }}</p><a class="read-more" href="{{ route('blog.public.show',$post->slug) }}">Read article →</a></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_news', true) && $latestNewsArticles->isNotEmpty())
|
||||
<section class="section alt"><div class="wrap"><div class="heading"><span>Newsroom</span><h2>Latest news</h2></div><div class="grid three">@foreach($latestNewsArticles as $article)<article class="card"><b>{{ optional($article->published_at)->format('d M Y') }}</b><h3>{{ $article->title }}</h3><p>{{ $article->excerpt }}</p><a class="read-more" href="{{ route('news.public.show',$article->slug) }}">Read update →</a></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_faq', true) && $faqs->isNotEmpty())
|
||||
<section class="section"><div class="wrap faq"><div><span class="section-label">FAQ</span><h2>Answers before you begin</h2></div><div>@foreach($faqs as $faq)<article><button type="button" @click="openFaq = openFaq === {{ $faq->id }} ? null : {{ $faq->id }}">{{ $faq->question }}<span>+</span></button><p x-cloak x-show="openFaq === {{ $faq->id }}">{{ $faq->answer }}</p></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_newsletter', true))
|
||||
<section class="section alt"><div class="wrap"><div class="content-card newsletter-panel"><div><span class="section-label">Newsletter</span><h2>Useful updates, no noise</h2><p>Receive product news, publishing guidance, and practical platform updates.</p></div><form method="POST" action="{{ route('newsletter.public.subscribe') }}">@csrf<input type="email" name="email" required placeholder="you@example.com" aria-label="Email address"><button class="btn primary" type="submit">Subscribe</button></form></div></div></section>
|
||||
@endif
|
||||
|
||||
<section class="cta" id="contact"><div class="wrap"><h2>{{ $themeSettings['cta_title'] ?? 'Ready to transform your business?' }}</h2><p>{{ $themeSettings['cta_description'] ?? 'Start building a faster, more connected operation today.' }}</p>@auth<a class="btn light" href="{{ route('dashboard') }}">Open dashboard →</a>@else @if(Route::has('register'))<a class="btn light" href="{{ route('register') }}">Get started free →</a>@endif @endauth</div></section>
|
||||
@if($hasComposition)
|
||||
{!! $contentRenderer->renderRegion($pageKey, 'body') !!}
|
||||
@else
|
||||
@include('stylist-theme-saasland::legacy-home-content')
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@@ -25,7 +25,16 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="saasland-theme" x-data="{ menuOpen: false, openFaq: null }">
|
||||
@include('stylist-theme-saasland::partials.header')
|
||||
@php
|
||||
$compositionRenderer = app(\Modules\Content\Services\PageRenderer::class);
|
||||
$compositionPageKey = $pageKey ?? null;
|
||||
$hasPageComposition = $compositionPageKey && $compositionRenderer->hasComposition($compositionPageKey);
|
||||
@endphp
|
||||
@if($hasPageComposition)
|
||||
{!! $compositionRenderer->renderRegion($compositionPageKey, 'header') !!}
|
||||
@else
|
||||
@include('stylist-theme-saasland::partials.header')
|
||||
@endif
|
||||
|
||||
@unless($hero)
|
||||
<section class="page-hero">
|
||||
@@ -52,7 +61,11 @@
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
@include('stylist-theme-saasland::partials.footer')
|
||||
@if($hasPageComposition)
|
||||
{!! $compositionRenderer->renderRegion($compositionPageKey, 'footer') !!}
|
||||
@else
|
||||
@include('stylist-theme-saasland::partials.footer')
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<script src="{{ route('stylist.theme.asset', ['theme' => 'saasland', 'path' => 'js/app.js']) }}" defer></script>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<section class="hero">
|
||||
<div class="glow one"></div><div class="glow two"></div>
|
||||
<div class="wrap hero-inner">
|
||||
<span class="eyebrow">{{ $themeSettings['hero_eyebrow'] ?? 'A better way to run your business' }}</span>
|
||||
<h1>{{ $heroTitle }}</h1>
|
||||
<p>{{ $heroText }}</p>
|
||||
<div class="actions">
|
||||
<a class="btn primary" href="{{ $themeSettings['primary_cta_url'] ?? (Route::has('register') ? route('register') : '#contact') }}">{{ $themeSettings['primary_cta_label'] ?? 'Get started free' }} →</a>
|
||||
<a class="btn secondary" href="{{ $themeSettings['secondary_cta_url'] ?? '#contact' }}">{{ $themeSettings['secondary_cta_label'] ?? 'Book demo' }}</a>
|
||||
</div>
|
||||
@if($enabled('show_metrics', true))
|
||||
<div class="dashboard">
|
||||
<div class="dash-top"><b>Performance overview</b><span>Live</span></div>
|
||||
<div class="metrics"><article><small>Monthly revenue</small><strong>£48,240</strong><em>+18.4%</em></article><article><small>Active users</small><strong>12,842</strong><em>+9.2%</em></article><article><small>Conversion</small><strong>7.8%</strong><em>+2.1%</em></article></div>
|
||||
<div class="chart" aria-label="Performance chart"><i style="height:28%"></i><i style="height:42%"></i><i style="height:35%"></i><i style="height:62%"></i><i style="height:54%"></i><i style="height:78%"></i><i style="height:92%"></i></div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if($enabled('show_features', true))
|
||||
<section class="section" id="features"><div class="wrap"><div class="heading"><span>Powerful features</span><h2>Everything your team needs to grow</h2><p>Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.</p></div><div class="grid three">
|
||||
@foreach([['Smart automation','Remove repetitive work and delays.'],['Real-time analytics','Turn activity into useful business insight.'],['Content operations','Create, review and publish confidently.'],['Secure by design','Protect users and data with dependable controls.'],['Flexible integrations','Connect the tools your organisation already uses.'],['Modular architecture','Activate only the capabilities each site needs.']] as $i => $feature)
|
||||
<article class="card"><b>{{ str_pad((string)($i+1),2,'0',STR_PAD_LEFT) }}</b><h3>{{ $feature[0] }}</h3><p>{{ $feature[1] }}</p></article>
|
||||
@endforeach
|
||||
</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_pricing', true) && $pricingPlans->isNotEmpty())
|
||||
<section class="section alt" id="pricing"><div class="wrap"><div class="heading"><span>Simple pricing</span><h2>Choose a plan that fits</h2><p>Plans are managed from Products & Pricing in the CMS.</p></div><div class="grid three pricing">
|
||||
@foreach($pricingPlans as $plan)
|
||||
<article class="card plan {{ $plan->is_featured ? 'featured' : '' }}">@if($plan->is_featured)<div class="popular">Most popular</div>@endif<h3>{{ $plan->name }}</h3><strong>@if($plan->price !== null){{ $plan->currency === 'GBP' ? '£' : $plan->currency.' ' }}{{ rtrim(rtrim(number_format((float)$plan->price,2), '0'), '.') }}<small>/{{ $plan->billing_period ?: 'month' }}</small>@else Custom @endif</strong><p>{{ $plan->description }}</p><ul>@foreach((array)$plan->features as $feature)<li>{{ $feature }}</li>@endforeach</ul><a class="btn {{ $plan->is_featured ? 'primary' : 'secondary' }}" href="{{ $plan->cta_url ?: '#contact' }}">{{ $plan->cta_label ?: 'Learn more' }}</a></article>
|
||||
@endforeach
|
||||
</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_testimonials', true) && $testimonials->isNotEmpty())
|
||||
<section class="section"><div class="wrap"><div class="heading"><span>Customer stories</span><h2>Trusted by ambitious teams</h2></div><div class="grid three">@foreach($testimonials as $testimonial)<blockquote class="card">“{{ $testimonial->content }}”<footer>{{ $testimonial->name }}@if($testimonial->company) · {{ $testimonial->company }}@endif</footer></blockquote>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_team', false) && $teamMembers->isNotEmpty())
|
||||
<section class="section alt"><div class="wrap"><div class="heading"><span>Our team</span><h2>People behind the platform</h2></div><div class="grid three">@foreach($teamMembers as $member)<article class="card"><h3>{{ $member->name }}</h3><b>{{ $member->job_title }}</b><p>{{ $member->biography }}</p></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_blog', true) && $latestBlogPosts->isNotEmpty())
|
||||
<section class="section"><div class="wrap"><div class="heading"><span>Insights</span><h2>Latest from the blog</h2></div><div class="grid three">@foreach($latestBlogPosts as $post)<article class="card"><b>{{ optional($post->published_at)->format('d M Y') }}</b><h3>{{ $post->title }}</h3><p>{{ $post->excerpt }}</p><a class="read-more" href="{{ route('blog.public.show',$post->slug) }}">Read article →</a></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_news', true) && $latestNewsArticles->isNotEmpty())
|
||||
<section class="section alt"><div class="wrap"><div class="heading"><span>Newsroom</span><h2>Latest news</h2></div><div class="grid three">@foreach($latestNewsArticles as $article)<article class="card"><b>{{ optional($article->published_at)->format('d M Y') }}</b><h3>{{ $article->title }}</h3><p>{{ $article->excerpt }}</p><a class="read-more" href="{{ route('news.public.show',$article->slug) }}">Read update →</a></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_faq', true) && $faqs->isNotEmpty())
|
||||
<section class="section"><div class="wrap faq"><div><span class="section-label">FAQ</span><h2>Answers before you begin</h2></div><div>@foreach($faqs as $faq)<article><button type="button" @click="openFaq = openFaq === {{ $faq->id }} ? null : {{ $faq->id }}">{{ $faq->question }}<span>+</span></button><p x-cloak x-show="openFaq === {{ $faq->id }}">{{ $faq->answer }}</p></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_newsletter', true))
|
||||
<section class="section alt"><div class="wrap"><div class="content-card newsletter-panel"><div><span class="section-label">Newsletter</span><h2>Useful updates, no noise</h2><p>Receive product news, publishing guidance, and practical platform updates.</p></div><form method="POST" action="{{ route('newsletter.public.subscribe') }}">@csrf<input type="email" name="email" required placeholder="you@example.com" aria-label="Email address"><button class="btn primary" type="submit">Subscribe</button></form></div></div></section>
|
||||
@endif
|
||||
|
||||
<section class="cta" id="contact"><div class="wrap"><h2>{{ $themeSettings['cta_title'] ?? 'Ready to transform your business?' }}</h2><p>{{ $themeSettings['cta_description'] ?? 'Start building a faster, more connected operation today.' }}</p>@auth<a class="btn light" href="{{ route('dashboard') }}">Open dashboard →</a>@else @if(Route::has('register'))<a class="btn light" href="{{ route('register') }}">Get started free →</a>@endif @endauth</div></section>
|
||||
@@ -0,0 +1,74 @@
|
||||
@extends('stylist-theme-saasland::layouts.app')
|
||||
|
||||
@php
|
||||
$title = $siteTitle;
|
||||
$description = $siteDescription;
|
||||
$hero = true;
|
||||
$enabled = fn (string $key, bool $default = true) => filter_var($themeSettings[$key] ?? $default, FILTER_VALIDATE_BOOL);
|
||||
$heroTitle = filled($themeSettings['hero_title'] ?? null) ? $themeSettings['hero_title'] : 'Transform Your Business With Our SaaS Solution';
|
||||
$heroText = filled($themeSettings['hero_description'] ?? null) ? $themeSettings['hero_description'] : 'Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams.';
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
<section class="hero">
|
||||
<div class="glow one"></div><div class="glow two"></div>
|
||||
<div class="wrap hero-inner">
|
||||
<span class="eyebrow">{{ $themeSettings['hero_eyebrow'] ?? 'A better way to run your business' }}</span>
|
||||
<h1>{{ $heroTitle }}</h1>
|
||||
<p>{{ $heroText }}</p>
|
||||
<div class="actions">
|
||||
<a class="btn primary" href="{{ $themeSettings['primary_cta_url'] ?? (Route::has('register') ? route('register') : '#contact') }}">{{ $themeSettings['primary_cta_label'] ?? 'Get started free' }} →</a>
|
||||
<a class="btn secondary" href="{{ $themeSettings['secondary_cta_url'] ?? '#contact' }}">{{ $themeSettings['secondary_cta_label'] ?? 'Book demo' }}</a>
|
||||
</div>
|
||||
@if($enabled('show_metrics', true))
|
||||
<div class="dashboard">
|
||||
<div class="dash-top"><b>Performance overview</b><span>Live</span></div>
|
||||
<div class="metrics"><article><small>Monthly revenue</small><strong>£48,240</strong><em>+18.4%</em></article><article><small>Active users</small><strong>12,842</strong><em>+9.2%</em></article><article><small>Conversion</small><strong>7.8%</strong><em>+2.1%</em></article></div>
|
||||
<div class="chart" aria-label="Performance chart"><i style="height:28%"></i><i style="height:42%"></i><i style="height:35%"></i><i style="height:62%"></i><i style="height:54%"></i><i style="height:78%"></i><i style="height:92%"></i></div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if($enabled('show_features', true))
|
||||
<section class="section" id="features"><div class="wrap"><div class="heading"><span>Powerful features</span><h2>Everything your team needs to grow</h2><p>Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.</p></div><div class="grid three">
|
||||
@foreach([['Smart automation','Remove repetitive work and delays.'],['Real-time analytics','Turn activity into useful business insight.'],['Content operations','Create, review and publish confidently.'],['Secure by design','Protect users and data with dependable controls.'],['Flexible integrations','Connect the tools your organisation already uses.'],['Modular architecture','Activate only the capabilities each site needs.']] as $i => $feature)
|
||||
<article class="card"><b>{{ str_pad((string)($i+1),2,'0',STR_PAD_LEFT) }}</b><h3>{{ $feature[0] }}</h3><p>{{ $feature[1] }}</p></article>
|
||||
@endforeach
|
||||
</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_pricing', true) && $pricingPlans->isNotEmpty())
|
||||
<section class="section alt" id="pricing"><div class="wrap"><div class="heading"><span>Simple pricing</span><h2>Choose a plan that fits</h2><p>Plans are managed from Products & Pricing in the CMS.</p></div><div class="grid three pricing">
|
||||
@foreach($pricingPlans as $plan)
|
||||
<article class="card plan {{ $plan->is_featured ? 'featured' : '' }}">@if($plan->is_featured)<div class="popular">Most popular</div>@endif<h3>{{ $plan->name }}</h3><strong>@if($plan->price !== null){{ $plan->currency === 'GBP' ? '£' : $plan->currency.' ' }}{{ rtrim(rtrim(number_format((float)$plan->price,2), '0'), '.') }}<small>/{{ $plan->billing_period ?: 'month' }}</small>@else Custom @endif</strong><p>{{ $plan->description }}</p><ul>@foreach((array)$plan->features as $feature)<li>{{ $feature }}</li>@endforeach</ul><a class="btn {{ $plan->is_featured ? 'primary' : 'secondary' }}" href="{{ $plan->cta_url ?: '#contact' }}">{{ $plan->cta_label ?: 'Learn more' }}</a></article>
|
||||
@endforeach
|
||||
</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_testimonials', true) && $testimonials->isNotEmpty())
|
||||
<section class="section"><div class="wrap"><div class="heading"><span>Customer stories</span><h2>Trusted by ambitious teams</h2></div><div class="grid three">@foreach($testimonials as $testimonial)<blockquote class="card">“{{ $testimonial->content }}”<footer>{{ $testimonial->name }}@if($testimonial->company) · {{ $testimonial->company }}@endif</footer></blockquote>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_team', false) && $teamMembers->isNotEmpty())
|
||||
<section class="section alt"><div class="wrap"><div class="heading"><span>Our team</span><h2>People behind the platform</h2></div><div class="grid three">@foreach($teamMembers as $member)<article class="card"><h3>{{ $member->name }}</h3><b>{{ $member->job_title }}</b><p>{{ $member->biography }}</p></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_blog', true) && $latestBlogPosts->isNotEmpty())
|
||||
<section class="section"><div class="wrap"><div class="heading"><span>Insights</span><h2>Latest from the blog</h2></div><div class="grid three">@foreach($latestBlogPosts as $post)<article class="card"><b>{{ optional($post->published_at)->format('d M Y') }}</b><h3>{{ $post->title }}</h3><p>{{ $post->excerpt }}</p><a class="read-more" href="{{ route('blog.public.show',$post->slug) }}">Read article →</a></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_news', true) && $latestNewsArticles->isNotEmpty())
|
||||
<section class="section alt"><div class="wrap"><div class="heading"><span>Newsroom</span><h2>Latest news</h2></div><div class="grid three">@foreach($latestNewsArticles as $article)<article class="card"><b>{{ optional($article->published_at)->format('d M Y') }}</b><h3>{{ $article->title }}</h3><p>{{ $article->excerpt }}</p><a class="read-more" href="{{ route('news.public.show',$article->slug) }}">Read update →</a></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_faq', true) && $faqs->isNotEmpty())
|
||||
<section class="section"><div class="wrap faq"><div><span class="section-label">FAQ</span><h2>Answers before you begin</h2></div><div>@foreach($faqs as $faq)<article><button type="button" @click="openFaq = openFaq === {{ $faq->id }} ? null : {{ $faq->id }}">{{ $faq->question }}<span>+</span></button><p x-cloak x-show="openFaq === {{ $faq->id }}">{{ $faq->answer }}</p></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
|
||||
@if($enabled('show_newsletter', true))
|
||||
<section class="section alt"><div class="wrap"><div class="content-card newsletter-panel"><div><span class="section-label">Newsletter</span><h2>Useful updates, no noise</h2><p>Receive product news, publishing guidance, and practical platform updates.</p></div><form method="POST" action="{{ route('newsletter.public.subscribe') }}">@csrf<input type="email" name="email" required placeholder="you@example.com" aria-label="Email address"><button class="btn primary" type="submit">Subscribe</button></form></div></div></section>
|
||||
@endif
|
||||
|
||||
<section class="cta" id="contact"><div class="wrap"><h2>{{ $themeSettings['cta_title'] ?? 'Ready to transform your business?' }}</h2><p>{{ $themeSettings['cta_description'] ?? 'Start building a faster, more connected operation today.' }}</p>@auth<a class="btn light" href="{{ route('dashboard') }}">Open dashboard →</a>@else @if(Route::has('register'))<a class="btn light" href="{{ route('register') }}">Get started free →</a>@endif @endauth</div></section>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user