From 4a32be068c19fb734144726236e403fcb18a6149 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Mon, 27 Jul 2026 11:45:03 +0100 Subject: [PATCH] fixed main frontend --- .../Console/InstallSaaslandContent.php | 138 ++++- Modules/Content/InstallSaaslandContent.php | 148 ++++++ .../Resources/views/admin/form.blade.php | 489 ++++++++++++++---- Modules/Content/views/admin/form.blade.php | 399 ++++++++++++++ .../views/views/documents/blog-feed.blade.php | 34 ++ .../views/documents/call-to-action.blade.php | 11 + .../views/views/documents/faq-feed.blade.php | 32 ++ .../views/documents/feature-grid.blade.php | 26 + .../views/views/documents/hero.blade.php | 46 ++ .../views/views/documents/news-feed.blade.php | 34 ++ .../views/documents/newsletter-form.blade.php | 20 + .../views/documents/product-feed.blade.php | 53 ++ .../views/documents/site-footer.blade.php | 3 + .../views/documents/site-header.blade.php | 3 + .../views/views/documents/team-feed.blade.php | 31 ++ .../documents/testimonial-feed.blade.php | 30 ++ .../views/views/partials/footer.blade.php | 37 ++ .../views/views/partials/header.blade.php | 69 +++ 18 files changed, 1471 insertions(+), 132 deletions(-) create mode 100644 Modules/Content/InstallSaaslandContent.php create mode 100644 Modules/Content/views/admin/form.blade.php create mode 100644 resources/themes/saasland/views/views/documents/blog-feed.blade.php create mode 100644 resources/themes/saasland/views/views/documents/call-to-action.blade.php create mode 100644 resources/themes/saasland/views/views/documents/faq-feed.blade.php create mode 100644 resources/themes/saasland/views/views/documents/feature-grid.blade.php create mode 100644 resources/themes/saasland/views/views/documents/hero.blade.php create mode 100644 resources/themes/saasland/views/views/documents/news-feed.blade.php create mode 100644 resources/themes/saasland/views/views/documents/newsletter-form.blade.php create mode 100644 resources/themes/saasland/views/views/documents/product-feed.blade.php create mode 100644 resources/themes/saasland/views/views/documents/site-footer.blade.php create mode 100644 resources/themes/saasland/views/views/documents/site-header.blade.php create mode 100644 resources/themes/saasland/views/views/documents/team-feed.blade.php create mode 100644 resources/themes/saasland/views/views/documents/testimonial-feed.blade.php create mode 100644 resources/themes/saasland/views/views/partials/footer.blade.php create mode 100644 resources/themes/saasland/views/views/partials/header.blade.php diff --git a/Modules/Content/Console/InstallSaaslandContent.php b/Modules/Content/Console/InstallSaaslandContent.php index 67fd9e0..27aa628 100644 --- a/Modules/Content/Console/InstallSaaslandContent.php +++ b/Modules/Content/Console/InstallSaaslandContent.php @@ -1,4 +1,5 @@ ! Schema::hasTable($table), - )); + $missingTables = array_values(array_filter($requiredTables, static fn (string $table): bool => ! Schema::hasTable($table))); if ($missingTables !== []) { $this->components->error('Content composition tables are missing: '.implode(', ', $missingTables).'.'); $this->components->info('Run php artisan migrate before installing the Saasland composition.'); - return self::FAILURE; } 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]); + if ($this->option('force')) { + PageContent::query()->where('page_key', 'home')->delete(); + } + + $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 ($this->definitions() as $key => $definition) { + $type = ContentType::query()->updateOrCreate(['key' => $key], $definition['type']); + $document = ContentDocument::query()->firstOrCreate( + ['slug' => 'saasland-'.$key], + [ + 'content_type_id' => $type->id, + 'title' => $definition['title'], + 'data' => $definition['data'], + 'status' => 'published', + 'published_at' => now(), + ] + ); + + if ((int) $document->content_type_id !== (int) $type->id) { + $document->forceFill(['content_type_id' => $type->id])->save(); + } + [$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.'); + + $this->components->info('Saasland content composition installed with editable schemas.'); 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'=>[]]; + $field = static fn (string $key, string $label, string $type = 'text', array $extra = []): array => array_merge(compact('key', 'label', 'type'), $extra); + $type = static fn (string $name, string $renderer, array $regions, array $schema): array => [ + 'name' => $name, 'renderer' => $renderer, 'allowed_regions' => $regions, + 'is_system' => true, 'is_active' => true, 'schema' => $schema, + ]; + $section = [$field('enabled', 'Show this section', 'boolean', ['default' => true])]; + $feed = array_merge($section, [ + $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), + $field('limit', 'Maximum items', 'number', ['min' => 1, 'max' => 50]), + $field('include_ids', 'Only show record IDs', 'csv', ['help' => 'Optional comma-separated IDs. Leave blank to use the latest records.']), + $field('exclude_ids', 'Exclude record IDs', 'csv', ['help' => 'Optional comma-separated IDs to hide from this section.']), + ]); + 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]], + 'site-header' => [ + 'title' => 'Main website header', + 'type' => $type('Site header', 'site-header', ['header'], [ + ...$section, $field('show_navigation', 'Show navigation', 'boolean', ['default' => true]), $field('show_login', 'Show login', 'boolean', ['default' => true]), + $field('show_registration', 'Show registration', 'boolean', ['default' => true]), $field('login_label', 'Login label'), + $field('registration_label', 'Registration label'), $field('sticky', 'Sticky header', 'boolean', ['default' => true]), + ]), + 'data' => ['enabled' => true, 'show_navigation' => true, 'show_login' => true, 'show_registration' => true, 'login_label' => 'Log in', 'registration_label' => 'Get started', 'sticky' => true], + ], + 'hero' => [ + 'title' => 'Homepage hero', + 'type' => $type('Hero', 'hero', ['body'], [ + ...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), + $field('primary_label', 'Primary button label'), $field('primary_url', 'Primary button URL', 'url'), + $field('secondary_label', 'Secondary button label'), $field('secondary_url', 'Secondary button URL', 'url'), + $field('show_metrics', 'Show metrics panel', 'boolean', ['default' => true]), + $field('metrics', 'Metrics', 'repeater', ['fields' => [ + $field('enabled', 'Show item', 'boolean', ['default' => true]), $field('label', 'Label'), $field('value', 'Value'), $field('change', 'Change'), $field('height', 'Chart height (%)', 'number', ['min' => 0, 'max' => 100]), + ]]), + ]), + 'data' => ['enabled' => true, '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, 'metrics' => [ + ['enabled' => true, 'label' => 'Monthly revenue', 'value' => '£48,240', 'change' => '+18.4%', 'height' => 62], + ['enabled' => true, 'label' => 'Active users', 'value' => '12,842', 'change' => '+9.2%', 'height' => 78], + ['enabled' => true, 'label' => 'Conversion', 'value' => '7.8%', 'change' => '+2.1%', 'height' => 92], + ]], + ], + 'feature-grid' => [ + 'title' => 'Core features', + 'type' => $type('Feature grid', 'feature-grid', ['body'], [ + ...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), + $field('items', 'Features', 'repeater', ['fields' => [$field('enabled', 'Show item', 'boolean', ['default' => true]), $field('title', 'Title'), $field('description', 'Description', 'textarea')]]), + ]), + 'data' => ['enabled' => true, 'eyebrow' => 'Powerful features', 'heading' => 'Everything your team needs to grow', 'description' => 'Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.', 'items' => [ + ['enabled' => true, 'title' => 'Smart automation', 'description' => 'Remove repetitive work and delays.'], + ['enabled' => true, 'title' => 'Real-time analytics', 'description' => 'Turn activity into useful business insight.'], + ['enabled' => true, 'title' => 'Content operations', 'description' => 'Create, review and publish confidently.'], + ['enabled' => true, 'title' => 'Secure by design', 'description' => 'Protect users and data with dependable controls.'], + ['enabled' => true, 'title' => 'Flexible integrations', 'description' => 'Connect the tools your organisation already uses.'], + ['enabled' => true, 'title' => 'Modular architecture', 'description' => 'Activate only the capabilities each site needs.'], + ]], + ], + 'product-feed' => ['title' => 'Pricing plans', 'type' => $type('Product feed', 'product-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Simple pricing', 'heading' => 'Choose a plan that fits', 'description' => 'Plans are managed from Products & Pricing in the CMS.', 'limit' => 6, 'include_ids' => '', 'exclude_ids' => '']], + 'testimonial-feed' => ['title' => 'Customer stories', 'type' => $type('Testimonial feed', 'testimonial-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Customer stories', 'heading' => 'Trusted by ambitious teams', 'description' => '', 'limit' => 6, 'include_ids' => '', 'exclude_ids' => '']], + 'team-feed' => ['title' => 'Team', 'type' => $type('Team feed', 'team-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Our team', 'heading' => 'People behind the platform', 'description' => '', 'limit' => 8, 'include_ids' => '', 'exclude_ids' => '']], + 'blog-feed' => ['title' => 'Latest blog posts', 'type' => $type('Blog feed', 'blog-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Insights', 'heading' => 'Latest from the blog', 'description' => '', 'limit' => 3, 'include_ids' => '', 'exclude_ids' => '']], + 'news-feed' => ['title' => 'Latest news', 'type' => $type('News feed', 'news-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Newsroom', 'heading' => 'Latest news', 'description' => '', 'limit' => 3, 'include_ids' => '', 'exclude_ids' => '']], + 'faq-feed' => ['title' => 'Frequently asked questions', 'type' => $type('FAQ feed', 'faq-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'FAQ', 'heading' => 'Answers before you begin', 'description' => '', 'limit' => 12, 'include_ids' => '', 'exclude_ids' => '']], + 'newsletter-form' => [ + 'title' => 'Newsletter signup', + 'type' => $type('Newsletter form', 'newsletter-form', ['body'], [...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), $field('placeholder', 'Email placeholder'), $field('button_label', 'Button label')]), + 'data' => ['enabled' => true, 'eyebrow' => 'Newsletter', 'heading' => 'Useful updates, no noise', 'description' => 'Receive product news, publishing guidance, and practical platform updates.', 'placeholder' => 'you@example.com', 'button_label' => 'Subscribe'], + ], + 'call-to-action' => [ + 'title' => 'Homepage final call to action', + 'type' => $type('Call to action', 'call-to-action', ['body'], [...$section, $field('anchor', 'Section anchor'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), $field('button_label', 'Button label'), $field('button_url', 'Button URL', 'url')]), + 'data' => ['enabled' => true, 'anchor' => 'contact', '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'], [...$section, $field('description', 'Description', 'textarea'), $field('show_navigation', 'Show footer navigation', 'boolean'), $field('show_account_links', 'Show account links', 'boolean', ['default' => true]), $field('show_social_links', 'Show social links', 'boolean', ['default' => true]), $field('show_back_to_top', 'Show back-to-top link', 'boolean', ['default' => true]), $field('copyright', 'Copyright text')]), + 'data' => ['enabled' => true, 'description' => 'One connected platform for modern teams.', 'show_navigation' => true, 'show_account_links' => true, 'show_social_links' => true, 'show_back_to_top' => true, 'copyright' => 'All rights reserved.'], + ], ]; } } diff --git a/Modules/Content/InstallSaaslandContent.php b/Modules/Content/InstallSaaslandContent.php new file mode 100644 index 0000000..27aa628 --- /dev/null +++ b/Modules/Content/InstallSaaslandContent.php @@ -0,0 +1,148 @@ + ! Schema::hasTable($table))); + + if ($missingTables !== []) { + $this->components->error('Content composition tables are missing: '.implode(', ', $missingTables).'.'); + $this->components->info('Run php artisan migrate before installing the Saasland composition.'); + return self::FAILURE; + } + + DB::transaction(function (): void { + if ($this->option('force')) { + PageContent::query()->where('page_key', 'home')->delete(); + } + + $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 ($this->definitions() as $key => $definition) { + $type = ContentType::query()->updateOrCreate(['key' => $key], $definition['type']); + $document = ContentDocument::query()->firstOrCreate( + ['slug' => 'saasland-'.$key], + [ + 'content_type_id' => $type->id, + 'title' => $definition['title'], + 'data' => $definition['data'], + 'status' => 'published', + 'published_at' => now(), + ] + ); + + if ((int) $document->content_type_id !== (int) $type->id) { + $document->forceFill(['content_type_id' => $type->id])->save(); + } + [$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 with editable schemas.'); + return self::SUCCESS; + } + + private function definitions(): array + { + $field = static fn (string $key, string $label, string $type = 'text', array $extra = []): array => array_merge(compact('key', 'label', 'type'), $extra); + $type = static fn (string $name, string $renderer, array $regions, array $schema): array => [ + 'name' => $name, 'renderer' => $renderer, 'allowed_regions' => $regions, + 'is_system' => true, 'is_active' => true, 'schema' => $schema, + ]; + $section = [$field('enabled', 'Show this section', 'boolean', ['default' => true])]; + $feed = array_merge($section, [ + $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), + $field('limit', 'Maximum items', 'number', ['min' => 1, 'max' => 50]), + $field('include_ids', 'Only show record IDs', 'csv', ['help' => 'Optional comma-separated IDs. Leave blank to use the latest records.']), + $field('exclude_ids', 'Exclude record IDs', 'csv', ['help' => 'Optional comma-separated IDs to hide from this section.']), + ]); + + return [ + 'site-header' => [ + 'title' => 'Main website header', + 'type' => $type('Site header', 'site-header', ['header'], [ + ...$section, $field('show_navigation', 'Show navigation', 'boolean', ['default' => true]), $field('show_login', 'Show login', 'boolean', ['default' => true]), + $field('show_registration', 'Show registration', 'boolean', ['default' => true]), $field('login_label', 'Login label'), + $field('registration_label', 'Registration label'), $field('sticky', 'Sticky header', 'boolean', ['default' => true]), + ]), + 'data' => ['enabled' => true, 'show_navigation' => true, 'show_login' => true, 'show_registration' => true, 'login_label' => 'Log in', 'registration_label' => 'Get started', 'sticky' => true], + ], + 'hero' => [ + 'title' => 'Homepage hero', + 'type' => $type('Hero', 'hero', ['body'], [ + ...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), + $field('primary_label', 'Primary button label'), $field('primary_url', 'Primary button URL', 'url'), + $field('secondary_label', 'Secondary button label'), $field('secondary_url', 'Secondary button URL', 'url'), + $field('show_metrics', 'Show metrics panel', 'boolean', ['default' => true]), + $field('metrics', 'Metrics', 'repeater', ['fields' => [ + $field('enabled', 'Show item', 'boolean', ['default' => true]), $field('label', 'Label'), $field('value', 'Value'), $field('change', 'Change'), $field('height', 'Chart height (%)', 'number', ['min' => 0, 'max' => 100]), + ]]), + ]), + 'data' => ['enabled' => true, '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, 'metrics' => [ + ['enabled' => true, 'label' => 'Monthly revenue', 'value' => '£48,240', 'change' => '+18.4%', 'height' => 62], + ['enabled' => true, 'label' => 'Active users', 'value' => '12,842', 'change' => '+9.2%', 'height' => 78], + ['enabled' => true, 'label' => 'Conversion', 'value' => '7.8%', 'change' => '+2.1%', 'height' => 92], + ]], + ], + 'feature-grid' => [ + 'title' => 'Core features', + 'type' => $type('Feature grid', 'feature-grid', ['body'], [ + ...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), + $field('items', 'Features', 'repeater', ['fields' => [$field('enabled', 'Show item', 'boolean', ['default' => true]), $field('title', 'Title'), $field('description', 'Description', 'textarea')]]), + ]), + 'data' => ['enabled' => true, 'eyebrow' => 'Powerful features', 'heading' => 'Everything your team needs to grow', 'description' => 'Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.', 'items' => [ + ['enabled' => true, 'title' => 'Smart automation', 'description' => 'Remove repetitive work and delays.'], + ['enabled' => true, 'title' => 'Real-time analytics', 'description' => 'Turn activity into useful business insight.'], + ['enabled' => true, 'title' => 'Content operations', 'description' => 'Create, review and publish confidently.'], + ['enabled' => true, 'title' => 'Secure by design', 'description' => 'Protect users and data with dependable controls.'], + ['enabled' => true, 'title' => 'Flexible integrations', 'description' => 'Connect the tools your organisation already uses.'], + ['enabled' => true, 'title' => 'Modular architecture', 'description' => 'Activate only the capabilities each site needs.'], + ]], + ], + 'product-feed' => ['title' => 'Pricing plans', 'type' => $type('Product feed', 'product-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Simple pricing', 'heading' => 'Choose a plan that fits', 'description' => 'Plans are managed from Products & Pricing in the CMS.', 'limit' => 6, 'include_ids' => '', 'exclude_ids' => '']], + 'testimonial-feed' => ['title' => 'Customer stories', 'type' => $type('Testimonial feed', 'testimonial-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Customer stories', 'heading' => 'Trusted by ambitious teams', 'description' => '', 'limit' => 6, 'include_ids' => '', 'exclude_ids' => '']], + 'team-feed' => ['title' => 'Team', 'type' => $type('Team feed', 'team-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Our team', 'heading' => 'People behind the platform', 'description' => '', 'limit' => 8, 'include_ids' => '', 'exclude_ids' => '']], + 'blog-feed' => ['title' => 'Latest blog posts', 'type' => $type('Blog feed', 'blog-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Insights', 'heading' => 'Latest from the blog', 'description' => '', 'limit' => 3, 'include_ids' => '', 'exclude_ids' => '']], + 'news-feed' => ['title' => 'Latest news', 'type' => $type('News feed', 'news-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Newsroom', 'heading' => 'Latest news', 'description' => '', 'limit' => 3, 'include_ids' => '', 'exclude_ids' => '']], + 'faq-feed' => ['title' => 'Frequently asked questions', 'type' => $type('FAQ feed', 'faq-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'FAQ', 'heading' => 'Answers before you begin', 'description' => '', 'limit' => 12, 'include_ids' => '', 'exclude_ids' => '']], + 'newsletter-form' => [ + 'title' => 'Newsletter signup', + 'type' => $type('Newsletter form', 'newsletter-form', ['body'], [...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), $field('placeholder', 'Email placeholder'), $field('button_label', 'Button label')]), + 'data' => ['enabled' => true, 'eyebrow' => 'Newsletter', 'heading' => 'Useful updates, no noise', 'description' => 'Receive product news, publishing guidance, and practical platform updates.', 'placeholder' => 'you@example.com', 'button_label' => 'Subscribe'], + ], + 'call-to-action' => [ + 'title' => 'Homepage final call to action', + 'type' => $type('Call to action', 'call-to-action', ['body'], [...$section, $field('anchor', 'Section anchor'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), $field('button_label', 'Button label'), $field('button_url', 'Button URL', 'url')]), + 'data' => ['enabled' => true, 'anchor' => 'contact', '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'], [...$section, $field('description', 'Description', 'textarea'), $field('show_navigation', 'Show footer navigation', 'boolean'), $field('show_account_links', 'Show account links', 'boolean', ['default' => true]), $field('show_social_links', 'Show social links', 'boolean', ['default' => true]), $field('show_back_to_top', 'Show back-to-top link', 'boolean', ['default' => true]), $field('copyright', 'Copyright text')]), + 'data' => ['enabled' => true, 'description' => 'One connected platform for modern teams.', 'show_navigation' => true, 'show_account_links' => true, 'show_social_links' => true, 'show_back_to_top' => true, 'copyright' => 'All rights reserved.'], + ], + ]; + } +} diff --git a/Modules/Content/Resources/views/admin/form.blade.php b/Modules/Content/Resources/views/admin/form.blade.php index ebaa180..47ae90d 100644 --- a/Modules/Content/Resources/views/admin/form.blade.php +++ b/Modules/Content/Resources/views/admin/form.blade.php @@ -1,120 +1,399 @@ -
-
-
-
- Content documents - / - {{ $document->exists ? 'Edit' : 'Create' }} +
+
+
+ + Content documents + +

+ {{ $document->exists ? $document->title : 'Create content document' }} +

+

+ Edit every frontend field below. JSON is retained only as an emergency advanced tool. +

-

{{ $document->exists ? $document->title : 'Create content document' }}

-

Structured, reusable content that can be placed into one or more page regions.

+ + @if ($document->exists) +
+
+ @csrf + Create independent copy +
+ +
+ @csrf + @method('DELETE') + Delete +
+
+ @endif
- @if($document->exists) -
-
- @csrf - Create independent copy -
-
- @csrf @method('DELETE') - Delete -
+ + @if (session('status')) +
+ {{ session('status') }}
@endif + + @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + + @if ($document->exists && $document->placements->count() > 1) +
+ Shared document. + Changes affect all {{ $document->placements->count() }} placements. +
+ @endif + +
+ @csrf + @if ($document->exists) + @method('PUT') + @endif + +
+ +

Document

+ + + + @foreach ($types as $type) + + @endforeach + +
+ + +
+
+

Frontend content

+

+ Switches take effect when this document is saved. +

+
+ +
+ +
+ +
+ Advanced JSON (emergency use only) +

+ Normal editing should be performed with the controls above. +

+ +
+
+
+ + +
- @if(session('status')) -
{{ session('status') }}
- @endif - @if($errors->any()) -
- The document could not be saved. -
    @foreach($errors->all() as $error)
  • {{ $error }}
  • @endforeach
-
- @endif + -@endpush + function sync() { + dataField.value = JSON.stringify(data, null, 2); + } + + function defaultValue(field) { + if (Object.prototype.hasOwnProperty.call(field, 'default')) return field.default; + if (field.type === 'boolean') return false; + if (field.type === 'repeater') return []; + return ''; + } + + function fieldMarkup(field, value, path) { + const help = field.help + ? `

${escapeHtml(field.help)}

` + : ''; + + if (field.type === 'boolean') { + return ` + `; + } + + if (field.type === 'textarea' || field.type === 'richtext') { + return ` + ${help}`; + } + + if (field.type === 'repeater') { + return repeaterMarkup(field, Array.isArray(value) ? value : [], path) + help; + } + + const type = field.type === 'number' ? 'number' : field.type === 'email' ? 'email' : 'text'; + return ` + ${help}`; + } + + function repeaterMarkup(field, items, path) { + return ` +
+
+
+

${escapeHtml(field.label)}

+

Items can be reordered, hidden, or removed.

+
+ +
+
+ ${items.length ? items.map((item, index) => ` +
+
+ Item ${index + 1} +
+ + + +
+
+
+ ${(field.fields || []).map(child => ` +
+ ${fieldMarkup(child, item?.[child.key] ?? defaultValue(child), `${path}.${index}.${child.key}`)} +
`).join('')} +
+
`).join('') : '

No items. Select “Add item” to create one.

'} +
+
`; + } + + function render() { + const schema = normaliseSchema(rawSchemas[String(typeSelect.value)]); + status.textContent = schema.length ? `${schema.length} editable field${schema.length === 1 ? '' : 's'}` : 'Schema unavailable'; + + if (!schema.length) { + host.innerHTML = ` +
+ This content type has no usable visual schema. Run php artisan content:install-saasland once to refresh schemas without replacing content. +
`; + return; + } + + host.innerHTML = schema.map(field => fieldMarkup( + field, + Object.prototype.hasOwnProperty.call(data, field.key) ? data[field.key] : defaultValue(field), + field.key + )).join(''); + + bind(); + } + + function bind() { + host.querySelectorAll('[data-path]').forEach(element => { + const eventName = element.type === 'checkbox' ? 'change' : 'input'; + element.addEventListener(eventName, () => { + let value = element.type === 'checkbox' ? element.checked : element.value; + if (element.type === 'number') value = element.value === '' ? null : Number(element.value); + set(element.dataset.path, value); + }); + }); + + host.querySelectorAll('[data-add]').forEach(button => { + button.addEventListener('click', () => { + const schema = normaliseSchema(rawSchemas[String(typeSelect.value)]); + const field = schema.find(candidate => candidate.key === button.dataset.add); + const items = Array.isArray(get(button.dataset.add)) ? get(button.dataset.add) : []; + const newItem = {}; + (field?.fields || []).forEach(child => newItem[child.key] = defaultValue(child)); + items.push(newItem); + set(button.dataset.add, items); + render(); + }); + }); + + host.querySelectorAll('[data-remove]').forEach(button => { + button.addEventListener('click', () => { + if (!confirm('Remove this item?')) return; + const parts = button.dataset.remove.split('.'); + const index = Number(parts.pop()); + const path = parts.join('.'); + const items = Array.isArray(get(path)) ? get(path) : []; + items.splice(index, 1); + set(path, items); + render(); + }); + }); + + host.querySelectorAll('[data-up], [data-down]').forEach(button => { + button.addEventListener('click', () => { + const raw = button.dataset.up ?? button.dataset.down; + const parts = raw.split('.'); + const index = Number(parts.pop()); + const path = parts.join('.'); + const items = Array.isArray(get(path)) ? get(path) : []; + const destination = button.dataset.up !== undefined ? index - 1 : index + 1; + if (destination < 0 || destination >= items.length) return; + [items[index], items[destination]] = [items[destination], items[index]]; + set(path, items); + render(); + }); + }); + } + + typeSelect.addEventListener('change', () => { + const schema = normaliseSchema(rawSchemas[String(typeSelect.value)]); + const nextData = {}; + schema.forEach(field => nextData[field.key] = defaultValue(field)); + data = nextData; + sync(); + render(); + }); + + dataField.addEventListener('change', () => { + try { + data = parseObject(dataField.value); + render(); + } catch (error) { + alert(`Invalid JSON: ${error.message}`); + } + }); + + form.addEventListener('submit', sync); + render(); + }); + diff --git a/Modules/Content/views/admin/form.blade.php b/Modules/Content/views/admin/form.blade.php new file mode 100644 index 0000000..47ae90d --- /dev/null +++ b/Modules/Content/views/admin/form.blade.php @@ -0,0 +1,399 @@ + +
+
+
+ + Content documents + +

+ {{ $document->exists ? $document->title : 'Create content document' }} +

+

+ Edit every frontend field below. JSON is retained only as an emergency advanced tool. +

+
+ + @if ($document->exists) +
+
+ @csrf + Create independent copy +
+ +
+ @csrf + @method('DELETE') + Delete +
+
+ @endif +
+ + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + + @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + + @if ($document->exists && $document->placements->count() > 1) +
+ Shared document. + Changes affect all {{ $document->placements->count() }} placements. +
+ @endif + +
+ @csrf + @if ($document->exists) + @method('PUT') + @endif + +
+ +

Document

+ + + + @foreach ($types as $type) + + @endforeach + +
+ + +
+
+

Frontend content

+

+ Switches take effect when this document is saved. +

+
+ +
+ +
+ +
+ Advanced JSON (emergency use only) +

+ Normal editing should be performed with the controls above. +

+ +
+
+
+ + +
+
+ + +
diff --git a/resources/themes/saasland/views/views/documents/blog-feed.blade.php b/resources/themes/saasland/views/views/documents/blog-feed.blade.php new file mode 100644 index 0000000..7192323 --- /dev/null +++ b/resources/themes/saasland/views/views/documents/blog-feed.blade.php @@ -0,0 +1,34 @@ +@if ((bool) ($data['enabled'] ?? true)) + @php + $includeIds = collect(explode(',', (string) ($data['include_ids'] ?? '')))->map(static fn ($id) => (int) trim($id))->filter(); + $excludeIds = collect(explode(',', (string) ($data['exclude_ids'] ?? '')))->map(static fn ($id) => (int) trim($id))->filter(); + $items = collect(${latestBlogPosts} ?? []) + ->when($includeIds->isNotEmpty(), static fn ($collection) => $collection->filter(static fn ($item) => $includeIds->contains((int) $item->id))) + ->reject(static fn ($item) => $excludeIds->contains((int) $item->id)) + ->take(max(1, (int) ($data['limit'] ?? 3))); + @endphp + + @if ($items->isNotEmpty()) +
+
+
+ @if (filled($data['eyebrow'] ?? null)){{ $data['eyebrow'] }}@endif + @if (filled($data['heading'] ?? null))

{{ $data['heading'] }}

@endif + @if (filled($data['description'] ?? null))

{{ $data['description'] }}

@endif +
+
+ @foreach ($items as $post) +
+ @if ($post->published_at){{ optional($post->published_at)->format('d M Y') }}@endif +

{{ $post->title }}

+ @if (filled($post->excerpt))

{{ $post->excerpt }}

@endif + @if (Route::has('blog.public.show')) + Read article → + @endif +
+ @endforeach +
+
+
+ @endif +@endif diff --git a/resources/themes/saasland/views/views/documents/call-to-action.blade.php b/resources/themes/saasland/views/views/documents/call-to-action.blade.php new file mode 100644 index 0000000..49faea1 --- /dev/null +++ b/resources/themes/saasland/views/views/documents/call-to-action.blade.php @@ -0,0 +1,11 @@ +@if ((bool) ($data['enabled'] ?? true)) +
+
+ @if (filled($data['heading'] ?? null))

{{ $data['heading'] }}

@endif + @if (filled($data['description'] ?? null))

{{ $data['description'] }}

@endif + @if (filled($data['button_label'] ?? null)) + {{ $data['button_label'] }} → + @endif +
+
+@endif diff --git a/resources/themes/saasland/views/views/documents/faq-feed.blade.php b/resources/themes/saasland/views/views/documents/faq-feed.blade.php new file mode 100644 index 0000000..be02c4f --- /dev/null +++ b/resources/themes/saasland/views/views/documents/faq-feed.blade.php @@ -0,0 +1,32 @@ +@if ((bool) ($data['enabled'] ?? true)) + @php + $includeIds = collect(explode(',', (string) ($data['include_ids'] ?? '')))->map(static fn ($id) => (int) trim($id))->filter(); + $excludeIds = collect(explode(',', (string) ($data['exclude_ids'] ?? '')))->map(static fn ($id) => (int) trim($id))->filter(); + $items = collect($faqs ?? []) + ->when($includeIds->isNotEmpty(), static fn ($collection) => $collection->filter(static fn ($item) => $includeIds->contains((int) $item->id))) + ->reject(static fn ($item) => $excludeIds->contains((int) $item->id)) + ->take(max(1, (int) ($data['limit'] ?? 12))); + @endphp + + @if ($items->isNotEmpty()) +
+
+
+ @if (filled($data['eyebrow'] ?? null))@endif + @if (filled($data['heading'] ?? null))

{{ $data['heading'] }}

@endif + @if (filled($data['description'] ?? null))

{{ $data['description'] }}

@endif +
+
+ @foreach ($items as $faq) +
+ +

{{ $faq->answer }}

+
+ @endforeach +
+
+
+ @endif +@endif diff --git a/resources/themes/saasland/views/views/documents/feature-grid.blade.php b/resources/themes/saasland/views/views/documents/feature-grid.blade.php new file mode 100644 index 0000000..7943d72 --- /dev/null +++ b/resources/themes/saasland/views/views/documents/feature-grid.blade.php @@ -0,0 +1,26 @@ +@if ((bool) ($data['enabled'] ?? true)) + @php + $items = collect($data['items'] ?? [])->filter(static fn ($item) => (bool) ($item['enabled'] ?? true)); + @endphp + + @if ($items->isNotEmpty()) +
+
+
+ @if (filled($data['eyebrow'] ?? null)){{ $data['eyebrow'] }}@endif + @if (filled($data['heading'] ?? null))

{{ $data['heading'] }}

@endif + @if (filled($data['description'] ?? null))

{{ $data['description'] }}

@endif +
+
+ @foreach ($items as $item) +
+ {{ str_pad((string) $loop->iteration, 2, '0', STR_PAD_LEFT) }} +

{{ $item['title'] ?? '' }}

+ @if (filled($item['description'] ?? null))

{{ $item['description'] }}

@endif +
+ @endforeach +
+
+
+ @endif +@endif diff --git a/resources/themes/saasland/views/views/documents/hero.blade.php b/resources/themes/saasland/views/views/documents/hero.blade.php new file mode 100644 index 0000000..1100f30 --- /dev/null +++ b/resources/themes/saasland/views/views/documents/hero.blade.php @@ -0,0 +1,46 @@ +@if ((bool) ($data['enabled'] ?? true)) + @php + $metrics = collect($data['metrics'] ?? [])->filter(static fn ($metric) => (bool) ($metric['enabled'] ?? true)); + @endphp + +
+
+
+
+ @if (filled($data['eyebrow'] ?? null)){{ $data['eyebrow'] }}@endif +

{{ $data['heading'] ?? $document->title }}

+ @if (filled($data['description'] ?? null))

{{ $data['description'] }}

@endif + + @if (filled($data['primary_label'] ?? null) || filled($data['secondary_label'] ?? null)) +
+ @if (filled($data['primary_label'] ?? null)) + {{ $data['primary_label'] }} → + @endif + @if (filled($data['secondary_label'] ?? null)) + {{ $data['secondary_label'] }} + @endif +
+ @endif + + @if ((bool) ($data['show_metrics'] ?? false) && $metrics->isNotEmpty()) +
+
Performance overviewLive
+
+ @foreach ($metrics as $metric) +
+ {{ $metric['label'] ?? '' }} + {{ $metric['value'] ?? '' }} + {{ $metric['change'] ?? '' }} +
+ @endforeach +
+
+ @foreach ($metrics as $metric) + + @endforeach +
+
+ @endif +
+
+@endif diff --git a/resources/themes/saasland/views/views/documents/news-feed.blade.php b/resources/themes/saasland/views/views/documents/news-feed.blade.php new file mode 100644 index 0000000..a36dcc6 --- /dev/null +++ b/resources/themes/saasland/views/views/documents/news-feed.blade.php @@ -0,0 +1,34 @@ +@if ((bool) ($data['enabled'] ?? true)) + @php + $includeIds = collect(explode(',', (string) ($data['include_ids'] ?? '')))->map(static fn ($id) => (int) trim($id))->filter(); + $excludeIds = collect(explode(',', (string) ($data['exclude_ids'] ?? '')))->map(static fn ($id) => (int) trim($id))->filter(); + $items = collect(${latestNewsArticles} ?? []) + ->when($includeIds->isNotEmpty(), static fn ($collection) => $collection->filter(static fn ($item) => $includeIds->contains((int) $item->id))) + ->reject(static fn ($item) => $excludeIds->contains((int) $item->id)) + ->take(max(1, (int) ($data['limit'] ?? 3))); + @endphp + + @if ($items->isNotEmpty()) +
+
+
+ @if (filled($data['eyebrow'] ?? null)){{ $data['eyebrow'] }}@endif + @if (filled($data['heading'] ?? null))

{{ $data['heading'] }}

@endif + @if (filled($data['description'] ?? null))

{{ $data['description'] }}

@endif +
+
+ @foreach ($items as $article) +
+ @if ($article->published_at){{ optional($article->published_at)->format('d M Y') }}@endif +

{{ $article->title }}

+ @if (filled($article->excerpt))

{{ $article->excerpt }}

@endif + @if (Route::has('news.public.show')) + Read update → + @endif +
+ @endforeach +
+
+
+ @endif +@endif diff --git a/resources/themes/saasland/views/views/documents/newsletter-form.blade.php b/resources/themes/saasland/views/views/documents/newsletter-form.blade.php new file mode 100644 index 0000000..e5f44e5 --- /dev/null +++ b/resources/themes/saasland/views/views/documents/newsletter-form.blade.php @@ -0,0 +1,20 @@ +@if ((bool) ($data['enabled'] ?? true)) +
+
+ +
+
+@endif diff --git a/resources/themes/saasland/views/views/documents/product-feed.blade.php b/resources/themes/saasland/views/views/documents/product-feed.blade.php new file mode 100644 index 0000000..5b814c3 --- /dev/null +++ b/resources/themes/saasland/views/views/documents/product-feed.blade.php @@ -0,0 +1,53 @@ +@if ((bool) ($data['enabled'] ?? true)) + @php + $includeIds = collect(explode(',', (string) ($data['include_ids'] ?? ''))) + ->map(static fn ($id) => (int) trim($id)) + ->filter() + ->values(); + $excludeIds = collect(explode(',', (string) ($data['exclude_ids'] ?? ''))) + ->map(static fn ($id) => (int) trim($id)) + ->filter() + ->values(); + $items = collect($pricingPlans ?? []) + ->when($includeIds->isNotEmpty(), static fn ($collection) => $collection->filter( + static fn ($item) => $includeIds->contains((int) $item->id) + )) + ->reject(static fn ($item) => $excludeIds->contains((int) $item->id)) + ->take(max(1, (int) ($data['limit'] ?? 6))); + @endphp + + @if ($items->isNotEmpty()) +
+
+
+ @if (filled($data['eyebrow'] ?? null)){{ $data['eyebrow'] }}@endif + @if (filled($data['heading'] ?? null))

{{ $data['heading'] }}

@endif + @if (filled($data['description'] ?? null))

{{ $data['description'] }}

@endif +
+
+ @foreach ($items as $plan) + + @endforeach +
+
+
+ @endif +@endif diff --git a/resources/themes/saasland/views/views/documents/site-footer.blade.php b/resources/themes/saasland/views/views/documents/site-footer.blade.php new file mode 100644 index 0000000..10a0e6e --- /dev/null +++ b/resources/themes/saasland/views/views/documents/site-footer.blade.php @@ -0,0 +1,3 @@ +@if ((bool) ($data['enabled'] ?? true)) + @include('stylist-theme-saasland::partials.footer', ['footerContent' => $data]) +@endif diff --git a/resources/themes/saasland/views/views/documents/site-header.blade.php b/resources/themes/saasland/views/views/documents/site-header.blade.php new file mode 100644 index 0000000..3f1ac4f --- /dev/null +++ b/resources/themes/saasland/views/views/documents/site-header.blade.php @@ -0,0 +1,3 @@ +@if ((bool) ($data['enabled'] ?? true)) + @include('stylist-theme-saasland::partials.header', ['headerContent' => $data]) +@endif diff --git a/resources/themes/saasland/views/views/documents/team-feed.blade.php b/resources/themes/saasland/views/views/documents/team-feed.blade.php new file mode 100644 index 0000000..d90b692 --- /dev/null +++ b/resources/themes/saasland/views/views/documents/team-feed.blade.php @@ -0,0 +1,31 @@ +@if ((bool) ($data['enabled'] ?? true)) + @php + $includeIds = collect(explode(',', (string) ($data['include_ids'] ?? '')))->map(static fn ($id) => (int) trim($id))->filter(); + $excludeIds = collect(explode(',', (string) ($data['exclude_ids'] ?? '')))->map(static fn ($id) => (int) trim($id))->filter(); + $items = collect($teamMembers ?? []) + ->when($includeIds->isNotEmpty(), static fn ($collection) => $collection->filter(static fn ($item) => $includeIds->contains((int) $item->id))) + ->reject(static fn ($item) => $excludeIds->contains((int) $item->id)) + ->take(max(1, (int) ($data['limit'] ?? 8))); + @endphp + + @if ($items->isNotEmpty()) +
+
+
+ @if (filled($data['eyebrow'] ?? null)){{ $data['eyebrow'] }}@endif + @if (filled($data['heading'] ?? null))

{{ $data['heading'] }}

@endif + @if (filled($data['description'] ?? null))

{{ $data['description'] }}

@endif +
+
+ @foreach ($items as $member) +
+

{{ $member->name }}

+ @if (filled($member->job_title)){{ $member->job_title }}@endif + @if (filled($member->biography))

{{ $member->biography }}

@endif +
+ @endforeach +
+
+
+ @endif +@endif diff --git a/resources/themes/saasland/views/views/documents/testimonial-feed.blade.php b/resources/themes/saasland/views/views/documents/testimonial-feed.blade.php new file mode 100644 index 0000000..91fef96 --- /dev/null +++ b/resources/themes/saasland/views/views/documents/testimonial-feed.blade.php @@ -0,0 +1,30 @@ +@if ((bool) ($data['enabled'] ?? true)) + @php + $includeIds = collect(explode(',', (string) ($data['include_ids'] ?? '')))->map(static fn ($id) => (int) trim($id))->filter(); + $excludeIds = collect(explode(',', (string) ($data['exclude_ids'] ?? '')))->map(static fn ($id) => (int) trim($id))->filter(); + $items = collect($testimonials ?? []) + ->when($includeIds->isNotEmpty(), static fn ($collection) => $collection->filter(static fn ($item) => $includeIds->contains((int) $item->id))) + ->reject(static fn ($item) => $excludeIds->contains((int) $item->id)) + ->take(max(1, (int) ($data['limit'] ?? 6))); + @endphp + + @if ($items->isNotEmpty()) +
+
+
+ @if (filled($data['eyebrow'] ?? null)){{ $data['eyebrow'] }}@endif + @if (filled($data['heading'] ?? null))

{{ $data['heading'] }}

@endif + @if (filled($data['description'] ?? null))

{{ $data['description'] }}

@endif +
+
+ @foreach ($items as $testimonial) +
+ “{{ $testimonial->content }}” +
{{ $testimonial->name }}@if ($testimonial->company) · {{ $testimonial->company }}@endif
+
+ @endforeach +
+
+
+ @endif +@endif diff --git a/resources/themes/saasland/views/views/partials/footer.blade.php b/resources/themes/saasland/views/views/partials/footer.blade.php new file mode 100644 index 0000000..2d851c2 --- /dev/null +++ b/resources/themes/saasland/views/views/partials/footer.blade.php @@ -0,0 +1,37 @@ +@php($footerContent = $footerContent ?? []) +
+ + + +
diff --git a/resources/themes/saasland/views/views/partials/header.blade.php b/resources/themes/saasland/views/views/partials/header.blade.php new file mode 100644 index 0000000..f66b8b1 --- /dev/null +++ b/resources/themes/saasland/views/views/partials/header.blade.php @@ -0,0 +1,69 @@ +@php($headerContent = $headerContent ?? []) + + +
+ @include('stylist-theme-saasland::partials.navigation-items', ['items' => $primaryNavigation]) + +
+ @auth + Dashboard + @if (Route::has('profile.edit')) + Profile + @endif +
+ @csrf + +
+ @else + @if (Route::has('login'))Log in@endif + @if (Route::has('register'))Get started@endif + @endauth +
+