front end fixes

This commit is contained in:
Mark Gregory
2026-07-27 07:37:11 +01:00
parent 0be7be48e7
commit 108f95dbfa
21 changed files with 687 additions and 423 deletions
@@ -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();
+2
View File
@@ -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>
@@ -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'],
];
}
}
+7 -1
View File
@@ -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);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Schema;
use Modules\Faq\Models\Faq;
use Modules\Products\Models\Product;
use Modules\Team\Models\TeamMember;
use Modules\Testimonials\Models\Testimonial;
final class SaaslandDemoSeeder extends Seeder
{
public function run(): void
{
if (Schema::hasTable('products')) {
foreach ([
['name' => 'Starter', 'price' => 19, 'description' => 'For individuals and small teams getting started.', 'features' => ['5 team members', 'Core analytics', 'Email support'], 'cta_label' => 'Start free', 'cta_url' => '/register', 'sort_order' => 10],
['name' => 'Growth', 'price' => 49, 'description' => 'For growing businesses that need more control.', 'features' => ['25 team members', 'Advanced analytics', 'Automations', 'Priority support'], 'cta_label' => 'Choose Growth', 'cta_url' => '/register', 'is_featured' => true, 'sort_order' => 20],
['name' => 'Scale', 'price' => null, 'description' => 'For established organisations with complex needs.', 'features' => ['Unlimited members', 'Custom integrations', 'Dedicated success manager'], 'cta_label' => 'Contact sales', 'cta_url' => '#contact', 'sort_order' => 30],
] as $plan) {
Product::query()->updateOrCreate(
['name' => $plan['name'], 'product_type' => 'plan'],
$plan + ['product_type' => 'plan', 'currency' => 'GBP', 'billing_period' => 'month', 'is_active' => true]
);
}
}
if (Schema::hasTable('testimonials')) {
foreach ([
['name' => 'Alex Morgan', 'company' => 'Northstar', 'content' => 'The platform gave us one clear view of the business and cut reporting time dramatically.', 'rating' => 5],
['name' => 'Priya Shah', 'company' => 'Lumina', 'content' => 'Our onboarding is faster, our handoffs are cleaner, and the team finally works from the same data.', 'rating' => 5],
['name' => 'Daniel Reed', 'company' => 'Orbit', 'content' => 'We launched in days and had enough insight to improve conversion in the first month.', 'rating' => 5],
] as $item) {
Testimonial::query()->updateOrCreate(['name' => $item['name'], 'company' => $item['company']], $item + ['enabled' => true]);
}
}
if (Schema::hasTable('faqs')) {
foreach ([
['question' => 'Can I change plan later?', 'answer' => 'Yes. Plans can be upgraded or changed whenever your requirements evolve.', 'sort_order' => 10],
['question' => 'Is my data secure?', 'answer' => 'The platform uses role-based access controls, secure sessions, audit logging, and production-ready deployment guidance.', 'sort_order' => 20],
['question' => 'Can the frontend be customised?', 'answer' => 'Yes. Theme settings control branding, content sections, navigation, colours, and public-site presentation.', 'sort_order' => 30],
] as $item) {
Faq::query()->updateOrCreate(['question' => $item['question']], $item + ['category' => 'General', 'enabled' => true]);
}
}
if (Schema::hasTable('team_members')) {
TeamMember::query()->updateOrCreate(
['name' => 'Product Team'],
['job_title' => 'Platform specialists', 'biography' => 'A multidisciplinary team focused on reliable, secure, and elegant digital experiences.', 'email' => null, 'sort_order' => 10, 'enabled' => true]
);
}
}
}
+206
View File
@@ -13,6 +13,7 @@
"vite": "^8.0.0"
},
"devDependencies": {
"fontaine": "^0.8.0",
"glob": "^11.0.0",
"sass": "^1.89.0"
},
@@ -22,6 +23,19 @@
"lightningcss-linux-x64-gnu": "^1.29.1"
}
},
"node_modules/@capsizecss/unpack": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz",
"integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"fontkitten": "^1.0.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
@@ -1023,6 +1037,26 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"devOptional": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -1170,6 +1204,13 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
"devOptional": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1185,6 +1226,20 @@
"node": ">= 8"
}
},
"node_modules/css-tree": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"mdn-data": "2.27.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1222,6 +1277,16 @@
"node": ">=6"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -1239,6 +1304,38 @@
}
}
},
"node_modules/fontaine": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/fontaine/-/fontaine-0.8.0.tgz",
"integrity": "sha512-eek1GbzOdWIj9FyQH/emqW1aEdfC3lYRCHepzwlFCm5T77fBSRSyNRKE6/antF1/B1M+SfJXVRQTY9GAr7lnDg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@capsizecss/unpack": "^4.0.0",
"css-tree": "^3.1.0",
"magic-regexp": "^0.10.0",
"magic-string": "^0.30.21",
"pathe": "^2.0.3",
"ufo": "^1.6.1",
"unplugin": "^2.3.10"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/fontkitten": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz",
"integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"tiny-inflate": "^1.0.3"
},
"engines": {
"node": ">=20"
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -1710,6 +1807,22 @@
"node": "20 || >=22"
}
},
"node_modules/magic-regexp": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.10.0.tgz",
"integrity": "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"estree-walker": "^3.0.3",
"magic-string": "^0.30.12",
"mlly": "^1.7.2",
"regexp-tree": "^0.1.27",
"type-level-regexp": "~0.1.17",
"ufo": "^1.5.4",
"unplugin": "^2.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1719,6 +1832,13 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mdn-data": {
"version": "2.27.1",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
"devOptional": true,
"license": "CC0-1.0"
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@@ -1745,6 +1865,19 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mlly": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
"integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.16.0",
"pathe": "^2.0.3",
"pkg-types": "^1.3.1",
"ufo": "^1.6.3"
}
},
"node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
@@ -1804,6 +1937,13 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"devOptional": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1822,6 +1962,18 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.1.8",
"mlly": "^1.7.4",
"pathe": "^2.0.1"
}
},
"node_modules/postcss": {
"version": "8.5.22",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
@@ -1864,6 +2016,16 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/regexp-tree": {
"version": "0.1.27",
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
"devOptional": true,
"license": "MIT",
"bin": {
"regexp-tree": "bin/regexp-tree"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -2053,6 +2215,13 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -2084,6 +2253,36 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-level-regexp": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz",
"integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/ufo": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/unplugin": {
"version": "2.3.11",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"acorn": "^8.15.0",
"picomatch": "^4.0.3",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/vite": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
@@ -2183,6 +2382,13 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+1
View File
@@ -19,6 +19,7 @@
"vite": "^8.0.0"
},
"devDependencies": {
"fontaine": "^0.8.0",
"glob": "^11.0.0",
"sass": "^1.89.0"
},
File diff suppressed because one or more lines are too long
+87 -2
View File
@@ -1,9 +1,9 @@
{
"name": "Saasland",
"slug": "saasland",
"version": "1.1.0",
"version": "1.2.0",
"author": "ABSDEV",
"description": "Dark SaaS marketing theme with integrated feature, pricing, privacy and terms sections.",
"description": "Configurable premium SaaS frontend with CMS-driven content, pricing, blog, news, team, testimonials, FAQ and newsletter sections.",
"entry_view": "home",
"stylesheet": "css/theme.css",
"script": "js/app.js",
@@ -86,6 +86,91 @@
"type": "url",
"label": "LinkedIn URL",
"default": ""
},
"hero_eyebrow": {
"type": "text",
"label": "Hero eyebrow",
"default": "A better way to run your business"
},
"hero_description": {
"type": "textarea",
"label": "Hero description",
"default": "Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams."
},
"primary_cta_label": {
"type": "text",
"label": "Primary button label",
"default": "Get started free"
},
"primary_cta_url": {
"type": "text",
"label": "Primary button URL",
"default": "/register"
},
"secondary_cta_label": {
"type": "text",
"label": "Secondary button label",
"default": "Book demo"
},
"secondary_cta_url": {
"type": "text",
"label": "Secondary button URL",
"default": "#contact"
},
"show_metrics": {
"type": "checkbox",
"label": "Show performance preview",
"default": true
},
"show_features": {
"type": "checkbox",
"label": "Show features",
"default": true
},
"show_pricing": {
"type": "checkbox",
"label": "Show pricing",
"default": true
},
"show_testimonials": {
"type": "checkbox",
"label": "Show testimonials",
"default": true
},
"show_team": {
"type": "checkbox",
"label": "Show team",
"default": false
},
"show_blog": {
"type": "checkbox",
"label": "Show blog",
"default": true
},
"show_news": {
"type": "checkbox",
"label": "Show news",
"default": true
},
"show_faq": {
"type": "checkbox",
"label": "Show FAQ",
"default": true
},
"show_newsletter": {
"type": "checkbox",
"label": "Show newsletter signup",
"default": true
},
"cta_title": {
"type": "text",
"label": "Final CTA title",
"default": "Ready to transform your business?"
},
"cta_description": {
"type": "text",
"label": "Final CTA description",
"default": "Start building a faster, more connected operation today."
}
},
"seo": {
+57 -373
View File
@@ -3,386 +3,70 @@
@php($description = $siteDescription)
@php($hero = true)
@section('content')
<section class="hero">
<div class="glow one"></div>
<div class="glow two"></div>
@php
$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 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>
<div class="wrap hero-inner">
<span class="eyebrow">Introducing Saasland 2.0</span>
@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
<h1>
Transform Your Business With Our
<span>SaaS Solution</span>
</h1>
@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
<p>
Streamline operations, boost productivity, and improve customer satisfaction
with one connected platform built for modern teams.
</p>
@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
<div class="actions">
<a class="btn primary" href="#pricing">Get Started Free →</a>
<a class="btn secondary" href="#contact">Book Demo</a>
</div>
@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
<div class="trust">
<div class="avatars" aria-hidden="true">
<i>A</i>
<i>B</i>
<i>C</i>
</div>
<small><b>500+</b> businesses already use our platform</small>
</div>
@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
<div class="dashboard">
<div class="dash-top">
<b>Performance overview</b>
<span>Live</span>
</div>
@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
<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>
@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
<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>
</div>
</section>
@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="section" id="features">
<div class="wrap">
<div class="heading">
<span>Powerful features</span>
<h2>Everything your team needs to grow</h2>
<p>
Move faster with flexible tools that bring planning, collaboration,
reporting, and customer insight into one workspace.
</p>
</div>
<div class="grid three">
<article class="card">
<b>01</b>
<h3>Smart automation</h3>
<p>Automate routine processes and let your team focus on work that creates value.</p>
</article>
<article class="card">
<b>02</b>
<h3>Real-time analytics</h3>
<p>Understand performance instantly with useful dashboards and actionable reporting.</p>
</article>
<article class="card">
<b>03</b>
<h3>Team collaboration</h3>
<p>Keep discussions, decisions, documents, and delivery aligned in one place.</p>
</article>
<article class="card">
<b>04</b>
<h3>Secure by design</h3>
<p>Protect business data with robust access controls and dependable infrastructure.</p>
</article>
<article class="card">
<b>05</b>
<h3>Easy integrations</h3>
<p>Connect the services you already use without rebuilding your existing workflow.</p>
</article>
<article class="card">
<b>06</b>
<h3>Priority support</h3>
<p>Get expert assistance when you need it from a team that understands your goals.</p>
</article>
</div>
</div>
</section>
<section class="section alt" id="roadmap">
<div class="wrap split">
<div>
<span class="section-label">Built for momentum</span>
<h2>From first idea to measurable growth</h2>
<p>
Launch quickly, learn from every interaction, and scale with confidence
using a platform designed around clear outcomes.
</p>
<ul class="checks">
<li>Fast, guided onboarding</li>
<li>Custom workflows for every team</li>
<li>Live performance and usage insight</li>
<li>Enterprise-ready controls</li>
</ul>
</div>
<div class="roadmap">
<article>
<span>01</span>
<div>
<h3>Connect</h3>
<p>Bring your data and tools together.</p>
</div>
</article>
<article>
<span>02</span>
<div>
<h3>Automate</h3>
<p>Remove repetitive work and delays.</p>
</div>
</article>
<article>
<span>03</span>
<div>
<h3>Optimise</h3>
<p>Use evidence to improve every cycle.</p>
</div>
</article>
</div>
</div>
</section>
<section class="section" id="pricing">
<div class="wrap">
<div class="heading">
<span>Simple pricing</span>
<h2>Choose a plan that fits</h2>
</div>
<div class="grid three pricing">
<article class="card plan">
<h3>Starter</h3>
<strong>£19<small>/month</small></strong>
<p>For individuals and small teams getting started.</p>
<ul>
<li>5 team members</li>
<li>Core analytics</li>
<li>Email support</li>
</ul>
<a class="btn secondary" href="#contact">Start free</a>
</article>
<article class="card plan featured">
<div class="popular">Most popular</div>
<h3>Growth</h3>
<strong>£49<small>/month</small></strong>
<p>For growing businesses that need more control.</p>
<ul>
<li>25 team members</li>
<li>Advanced analytics</li>
<li>Automations</li>
<li>Priority support</li>
</ul>
<a class="btn primary" href="#contact">Choose Growth</a>
</article>
<article class="card plan">
<h3>Scale</h3>
<strong>Custom</strong>
<p>For established organisations with complex needs.</p>
<ul>
<li>Unlimited members</li>
<li>Custom integrations</li>
<li>Dedicated success manager</li>
</ul>
<a class="btn secondary" href="#contact">Contact sales</a>
</article>
</div>
</div>
</section>
<section class="section alt">
<div class="wrap">
<div class="heading">
<span>Customer stories</span>
<h2>Trusted by ambitious teams</h2>
</div>
<div class="grid three">
<blockquote class="card">
“Saasland gave us one clear view of the business and cut reporting time dramatically.”
<footer>Alex Morgan · Northstar</footer>
</blockquote>
<blockquote class="card">
“Our onboarding is faster, our handoffs are cleaner, and the team finally works from the same data.”
<footer>Priya Shah · Lumina</footer>
</blockquote>
<blockquote class="card">
“We launched in days and had enough insight to improve conversion in the first month.”
<footer>Daniel Reed · Orbit</footer>
</blockquote>
</div>
</div>
</section>
<section class="section">
<div class="wrap faq">
<div>
<span class="section-label">FAQ</span>
<h2>Answers before you begin</h2>
</div>
<div>
<article>
<button type="button" @click="openFaq = openFaq === 1 ? null : 1">
Can I try Saasland free?
<span>+</span>
</button>
<p x-show="openFaq === 1">
Yes. Start with a free trial and move to a paid plan when your team is ready.
</p>
</article>
<article>
<button type="button" @click="openFaq = openFaq === 2 ? null : 2">
Can I change plans later?
<span>+</span>
</button>
<p x-show="openFaq === 2">
You can upgrade or change your plan as your requirements evolve.
</p>
</article>
<article>
<button type="button" @click="openFaq = openFaq === 3 ? null : 3">
Does it integrate with existing tools?
<span>+</span>
</button>
<p x-show="openFaq === 3">
Yes. Saasland is designed to connect with common business platforms and custom services.
</p>
</article>
</div>
</div>
</section>
<section class="section alt legal-area" id="privacy">
<div class="wrap">
<div class="heading">
<span>Privacy</span>
<h2>Your data, handled responsibly</h2>
<p>
We collect only the information needed to provide, secure, and improve
Saasland. We do not sell your personal information.
</p>
</div>
<div class="grid three legal-grid">
<article class="card legal-card">
<b>01</b>
<h3>Information we collect</h3>
<p>
Account details, contact information, billing records, product usage,
support messages, and technical data such as browser and device details.
</p>
</article>
<article class="card legal-card">
<b>02</b>
<h3>How we use it</h3>
<p>
To operate the service, process payments, protect accounts, provide
support, improve features, and meet legal obligations.
</p>
</article>
<article class="card legal-card">
<b>03</b>
<h3>Your choices</h3>
<p>
You may request access, correction, export, or deletion of your data,
and you can change communication preferences at any time.
</p>
</article>
</div>
<div class="legal-note">
<strong>Privacy contact</strong>
<p>
Questions about privacy can be sent to
<a href="mailto:privacy@example.com">privacy@example.com</a>.
</p>
</div>
</div>
</section>
<section class="section legal-area" id="terms">
<div class="wrap">
<div class="heading">
<span>Terms</span>
<h2>Clear terms for using Saasland</h2>
<p>
By creating an account or using the service, you agree to use Saasland
lawfully and in accordance with these core terms.
</p>
</div>
<div class="grid three legal-grid">
<article class="card legal-card">
<b>01</b>
<h3>Account responsibilities</h3>
<p>
Keep account details accurate, protect login credentials, and ensure
activity under your account follows applicable laws and these terms.
</p>
</article>
<article class="card legal-card">
<b>02</b>
<h3>Subscriptions and billing</h3>
<p>
Paid plans renew according to the selected billing period. Fees are due
when stated and plan changes take effect under the displayed terms.
</p>
</article>
<article class="card legal-card">
<b>03</b>
<h3>Acceptable use</h3>
<p>
Do not misuse the platform, interfere with service availability, access
data without permission, or use Saasland for unlawful activity.
</p>
</article>
</div>
<div class="legal-note">
<strong>Legal contact</strong>
<p>
Questions about these terms can be sent to
<a href="mailto:legal@example.com">legal@example.com</a>.
</p>
</div>
</div>
</section>
<section class="cta" id="contact">
<div class="wrap">
<h2>Ready to transform your business?</h2>
<p>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>
<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
@@ -1,10 +1,11 @@
@php
$title = $title ?? null;
$description = $description ?? null;
$eyebrow = $eyebrow ?? null;
$hero = $hero ?? false;
$breadcrumbs = $breadcrumbs ?? [];
@endphp
@props([
'title' => null,
'description' => null,
'eyebrow' => null,
'hero' => false,
'breadcrumbs' => [],
])
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" id="top">
<head>
@@ -49,7 +50,7 @@
@endunless
<main class="{{ $hero ? 'marketing-main' : 'page-main' }}">
@yield('content')
{{ $slot }}
</main>
@include('stylist-theme-saasland::partials.footer')