Add all files
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\Pages;
|
||||
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\ProductPageResource;
|
||||
|
||||
class CreateProductPage extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ProductPageResource::class;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\Pages;
|
||||
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\ProductPageResource;
|
||||
|
||||
class EditProductPage extends EditRecord
|
||||
{
|
||||
protected static string $resource = ProductPageResource::class;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\Pages;
|
||||
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources\ProductPageResource;
|
||||
|
||||
class ListProductPages extends ListRecords
|
||||
{
|
||||
protected static string $resource = ProductPageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Admin\Resources;
|
||||
|
||||
use App\Models\Category;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Models\ProductPageSettings;
|
||||
|
||||
class ProductPageResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ProductPageSettings::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'ri-pages-line';
|
||||
|
||||
protected static ?string $navigationGroup = 'Content';
|
||||
|
||||
protected static ?string $navigationLabel = 'Product Pages';
|
||||
|
||||
protected static ?int $navigationSort = 16;
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Product Page';
|
||||
|
||||
protected static ?string $modelLabel = 'Product Page';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make('Basic Information')
|
||||
->schema([
|
||||
FileUpload::make('hero_section.background')
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('product-pages/hero')
|
||||
->required()
|
||||
->label('Background Image'),
|
||||
TextInput::make('hero_section.title')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->label('Title')
|
||||
->afterStateUpdated(function ($state, callable $set) {
|
||||
$set('hero_section.slug', \Str::slug($state));
|
||||
$set('seo.title', $state);
|
||||
}),
|
||||
TextInput::make('hero_section.slug')
|
||||
->required()
|
||||
->unique(
|
||||
table: 'ext_product_page_builder',
|
||||
column: 'hero_section->slug',
|
||||
ignorable: fn ($record) => $record
|
||||
)
|
||||
->label('URL Slug'),
|
||||
RichEditor::make('hero_section.description')
|
||||
->required()
|
||||
->label('Description')
|
||||
->afterStateUpdated(function ($state, callable $set) {
|
||||
$set('seo.description', strip_tags($state));
|
||||
}),
|
||||
Select::make('category_id')
|
||||
->label('Category')
|
||||
->options(Category::all()->pluck('name', 'id'))
|
||||
->required(),
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('SEO Settings')
|
||||
->schema([
|
||||
TextInput::make('seo.title')
|
||||
->label('SEO Title')
|
||||
->placeholder('Will use Basic Information title if left empty'),
|
||||
TextInput::make('seo.url')
|
||||
->label('Canonical URL')
|
||||
->placeholder('Auto-generated from slug')
|
||||
->dehydrated(false)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($record) => $record ? url('/product/' . ($record->hero_section['slug'] ?? '')) : ''
|
||||
),
|
||||
FileUpload::make('seo.banner')
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('product-pages/seo')
|
||||
->label('Social Media Banner'),
|
||||
Textarea::make('seo.description')
|
||||
->label('SEO Description')
|
||||
->placeholder('Will use Basic Information description if left empty')
|
||||
->rows(3),
|
||||
/*
|
||||
==================================================================================
|
||||
This only work if you inside themes/default/views/layouts/app.blade.php
|
||||
|
||||
{{--
|
||||
@isset($keywords)
|
||||
<meta name="keywords" content="{{ $keywords }}">
|
||||
@endisset
|
||||
--}}
|
||||
|
||||
delete {{-- and --}}
|
||||
|
||||
and also in extension/Others/ProductPageBuilder/resources/views/show.blade.php
|
||||
|
||||
{{--
|
||||
<x-slot name="keywords">{{ is_array($page->seo['keywords'] ?? null) ? implode(', ', $page->seo['keywords']) : ($page->seo['keywords'] ?? '') }}</x-slot>
|
||||
--}}
|
||||
|
||||
delete {{-- and --}}
|
||||
|
||||
TagsInput::make('seo.keywords')
|
||||
->label('SEO Keywords')
|
||||
->placeholder('Add keywords and press enter')
|
||||
->separator(','),
|
||||
|
||||
==================================================================================
|
||||
*/
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('Features')
|
||||
->schema([
|
||||
Repeater::make('features')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Select::make('icon_type')
|
||||
->label('Icon Type')
|
||||
->options([
|
||||
'image' => 'Image',
|
||||
'svg' => 'SVG',
|
||||
])
|
||||
->default('image')
|
||||
->required()
|
||||
->reactive()
|
||||
->formatStateUsing(function ($state, $record) {
|
||||
if (!empty($state)) {
|
||||
return $state;
|
||||
}
|
||||
if (!empty($record['icon_svg'])) {
|
||||
return 'svg';
|
||||
}
|
||||
if (!empty($record['icon'])) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
return 'image';
|
||||
}),
|
||||
Textarea::make('icon_svg')
|
||||
->label('SVG Code')
|
||||
->rows(5)
|
||||
->placeholder('<svg ...>...</svg>')
|
||||
->visible(fn ($get) => $get('icon_type') === 'svg'),
|
||||
FileUpload::make('icon')
|
||||
->directory('product-pages/features')
|
||||
->visible(fn ($get) => $get('icon_type') === 'image'),
|
||||
TextInput::make('title'),
|
||||
// ->required(),
|
||||
RichEditor::make('description'),
|
||||
// ->required(),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
]),
|
||||
// ->required()
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('Game Preview')
|
||||
->schema([
|
||||
Repeater::make('game_preview')
|
||||
->maxItems(1)
|
||||
->schema([
|
||||
TextInput::make('title'),
|
||||
// ->required(),
|
||||
RichEditor::make('description'),
|
||||
// ->required(),
|
||||
TextInput::make('youtube_embed')
|
||||
// ->required()
|
||||
->placeholder('e.g. https://www.youtube.com/embed/xxxxx'),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
]),
|
||||
// ->required()
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('Game Panel')
|
||||
->schema([
|
||||
Repeater::make('game_panel')
|
||||
->maxItems(1)
|
||||
->schema([
|
||||
TextInput::make('title'),
|
||||
// ->required(),
|
||||
RichEditor::make('description'),
|
||||
// ->required(),
|
||||
FileUpload::make('gallery')
|
||||
// ->required()
|
||||
->multiple()
|
||||
->directory('product-pages/gallery'),
|
||||
TagsInput::make('features')
|
||||
->label('Panel Features')
|
||||
->placeholder('Add feature and press enter')
|
||||
->separator(','),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
]),
|
||||
// ->required()
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('FAQs')
|
||||
->schema([
|
||||
Repeater::make('faqs')
|
||||
->collapsible()
|
||||
->schema([
|
||||
TextInput::make('question'),
|
||||
// ->required(),
|
||||
RichEditor::make('answer'),
|
||||
// ->required(),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
]),
|
||||
// ->required()
|
||||
])
|
||||
->collapsible(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->label('Last Updated')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('hero_section.title')
|
||||
->label('Title'),
|
||||
Tables\Columns\TextColumn::make('hero_section.slug')
|
||||
->label('Slug'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\ViewAction::make()
|
||||
->url(fn ($record) => "/games/{$record->hero_section['slug']}")
|
||||
->openUrlInNewTab(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
/*
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()
|
||||
->label('Create Product Page')
|
||||
])*/
|
||||
->defaultSort('updated_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListProductPages::route('/'),
|
||||
'create' => Pages\CreateProductPage::route('/create'),
|
||||
'edit' => Pages\EditProductPage::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Models\ProductPageSettings;
|
||||
|
||||
class ProductPageController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$pages = ProductPageSettings::all();
|
||||
|
||||
return view('product_page_builder::index', compact('pages'));
|
||||
}
|
||||
|
||||
public function show($slug)
|
||||
{
|
||||
$page = ProductPageSettings::whereJsonContains('hero_section->slug', $slug)->firstOrFail();
|
||||
|
||||
return view('product_page_builder::show', compact('page'));
|
||||
}
|
||||
}
|
||||
70
Disabled/ProductPageBuilder/Models/ProductPageSettings.php
Normal file
70
Disabled/ProductPageBuilder/Models/ProductPageSettings.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder\Models;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProductPageSettings extends Model
|
||||
{
|
||||
protected $table = 'ext_product_page_builder';
|
||||
|
||||
protected $fillable = [
|
||||
'hero_section',
|
||||
'features',
|
||||
'pricing_tables',
|
||||
'seo',
|
||||
'category_id',
|
||||
'game_preview',
|
||||
'game_panel',
|
||||
'faqs',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'hero_section' => 'array',
|
||||
'features' => 'array',
|
||||
'pricing_tables' => 'array',
|
||||
'seo' => 'array',
|
||||
'game_preview' => 'array',
|
||||
'game_panel' => 'array',
|
||||
'faqs' => 'array',
|
||||
];
|
||||
|
||||
protected function heroSection(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn ($value) => json_decode($value, true) ?? [],
|
||||
set: fn ($value) => json_encode($value),
|
||||
);
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
public function getMinPriceDetailsAttribute()
|
||||
{
|
||||
if (!$this->category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$minPrice = $this->category->products()
|
||||
->join('plans', function ($join) {
|
||||
$join->on('plans.priceable_id', '=', 'products.id')
|
||||
->where('plans.priceable_type', '=', 'App\\Models\\Product');
|
||||
})
|
||||
->join('prices', function ($join) {
|
||||
$join->on('prices.plan_id', '=', 'plans.id');
|
||||
})
|
||||
->select('prices.price', 'prices.currency_code')
|
||||
->orderBy('prices.price', 'asc')
|
||||
->first();
|
||||
|
||||
return $minPrice ? [
|
||||
'price' => $minPrice->price,
|
||||
'currency_code' => $minPrice->currency_code,
|
||||
] : null;
|
||||
}
|
||||
}
|
||||
89
Disabled/ProductPageBuilder/ProductPageBuilder.php
Normal file
89
Disabled/ProductPageBuilder/ProductPageBuilder.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder;
|
||||
|
||||
use App\Classes\Extension\Extension;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Models\ProductPageSettings;
|
||||
|
||||
class ProductPageBuilder extends Extension
|
||||
{
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'notice',
|
||||
'type' => 'placeholder',
|
||||
'label' => 'Use this extension to customize your product pages with custom sections.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function enabled()
|
||||
{
|
||||
Artisan::call('migrate', [
|
||||
'--path' => 'extensions/Others/ProductPageBuilder/database/migrations',
|
||||
'--force' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
// Add category_id column if it doesn't exist
|
||||
if (!$this->columnExists('ext_product_page_builder', 'category_id')) {
|
||||
DB::statement('ALTER TABLE ext_product_page_builder ADD COLUMN category_id bigint unsigned NULL');
|
||||
DB::statement('ALTER TABLE ext_product_page_builder ADD CONSTRAINT product_page_builder_category_id_foreign FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL');
|
||||
}
|
||||
|
||||
// Register views
|
||||
View::addNamespace('product_page_builder', __DIR__ . '/resources/views');
|
||||
|
||||
// Register routes
|
||||
Route::middleware('web')->group(__DIR__ . '/routes/web.php');
|
||||
|
||||
// Register Filament resources
|
||||
if (class_exists('\Filament\Facades\Filament')) {
|
||||
Filament::registerResources([
|
||||
Admin\Resources\ProductPageResource::class,
|
||||
]);
|
||||
}
|
||||
|
||||
Event::listen('product.page', function ($product) {
|
||||
$settings = ProductPageSettings::first();
|
||||
if (!$settings) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'view' => view('product_page_builder::product', [
|
||||
'settings' => $settings,
|
||||
'product' => $product,
|
||||
]),
|
||||
];
|
||||
});
|
||||
|
||||
Event::listen('navigation', function () {
|
||||
if (ProductPageSettings::count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => 'Games',
|
||||
'route' => 'product-page.index',
|
||||
// 'icon' => '',
|
||||
'separator' => true,
|
||||
'children' => [],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function columnExists($table, $column)
|
||||
{
|
||||
return DB::getSchemaBuilder()->hasColumn($table, $column);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\ProductPageBuilder;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ProductPageBuilderServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
$this->loadRoutesFrom(__DIR__ . '/routes/web.php');
|
||||
$this->loadMigrationsFrom(__DIR__ . '/database/migrations');
|
||||
$this->loadViewsFrom(__DIR__ . '/resources/views', 'product-page-builder');
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
10
Disabled/ProductPageBuilder/composer.json
Normal file
10
Disabled/ProductPageBuilder/composer.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "paymenter/product-page-builder",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Paymenter\\Extensions\\Others\\ProductPageBuilder\\ProductPageBuilderServiceProvider"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::dropIfExists('ext_product_page_builder');
|
||||
|
||||
Schema::create('ext_product_page_builder', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('hero_section')->nullable();
|
||||
$table->json('features')->nullable();
|
||||
$table->json('seo')->nullable();
|
||||
$table->json('game_preview')->nullable();
|
||||
$table->json('game_panel')->nullable();
|
||||
$table->json('faqs')->nullable();
|
||||
$table->foreignId('category_id')->nullable()->constrained('categories')->onDelete('set null');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('ext_product_page_builder');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
@props([
|
||||
'options' => [],
|
||||
'placeholder' => 'Select option'
|
||||
])
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
selectOpen: false,
|
||||
selectedValue: @entangle($attributes->wire('model')),
|
||||
selectableItems: {{ json_encode($options) }},
|
||||
selectableItemActive: null,
|
||||
selectId: $id('select'),
|
||||
selectDropdownPosition: 'bottom',
|
||||
|
||||
init() {
|
||||
this.$watch('selectOpen', () => {
|
||||
if (this.selectOpen) {
|
||||
this.selectableItemActive = this.selectableItems.find(item => item.value === this.selectedValue)
|
||||
|| this.selectableItems[0];
|
||||
this.$nextTick(() => this.selectScrollToActiveItem());
|
||||
}
|
||||
this.$nextTick(() => this.selectPositionUpdate());
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => this.selectPositionUpdate());
|
||||
},
|
||||
|
||||
selectableItemIsActive(item) {
|
||||
return this.selectableItemActive && this.selectableItemActive.value === item.value;
|
||||
},
|
||||
|
||||
selectableItemActiveNext() {
|
||||
let index = this.selectableItems.indexOf(this.selectableItemActive);
|
||||
if (index < this.selectableItems.length - 1) {
|
||||
this.selectableItemActive = this.selectableItems[index + 1];
|
||||
this.selectScrollToActiveItem();
|
||||
}
|
||||
},
|
||||
|
||||
selectableItemActivePrevious() {
|
||||
let index = this.selectableItems.indexOf(this.selectableItemActive);
|
||||
if (index > 0) {
|
||||
this.selectableItemActive = this.selectableItems[index - 1];
|
||||
this.selectScrollToActiveItem();
|
||||
}
|
||||
},
|
||||
|
||||
selectScrollToActiveItem() {
|
||||
if (this.selectableItemActive) {
|
||||
const activeElement = document.getElementById(this.selectableItemActive.value + '-' + this.selectId);
|
||||
if (activeElement) {
|
||||
activeElement.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
selectPositionUpdate() {
|
||||
if (!this.$refs.selectButton || !this.$refs.selectableItemsList) return;
|
||||
|
||||
const selectDropdownBottomPos = this.$refs.selectButton.getBoundingClientRect().top +
|
||||
this.$refs.selectButton.offsetHeight +
|
||||
this.$refs.selectableItemsList.offsetHeight;
|
||||
|
||||
this.selectDropdownPosition = window.innerHeight < selectDropdownBottomPos ? 'top' : 'bottom';
|
||||
}
|
||||
}"
|
||||
@keydown.escape="selectOpen = false"
|
||||
@keydown.down.prevent="if(selectOpen) { selectableItemActiveNext() } else { selectOpen = true }"
|
||||
@keydown.up.prevent="if(selectOpen) { selectableItemActivePrevious() } else { selectOpen = true }"
|
||||
@keydown.enter.prevent="selectedValue = selectableItemActive.value; selectOpen = false"
|
||||
class="relative w-full"
|
||||
>
|
||||
<button
|
||||
x-ref="selectButton"
|
||||
@click="selectOpen = !selectOpen"
|
||||
:class="{ 'ring-2 ring-offset-2 ring-neutral-800': selectOpen }"
|
||||
class="relative w-full min-h-[38px] py-2 pl-3 pr-10 text-left bg-background-secondary border border-neutral rounded-md shadow-sm cursor-pointer text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-800"
|
||||
type="button"
|
||||
>
|
||||
<span x-text="selectableItems.find(item => item.value === selectedValue)?.label ?? '{{ $placeholder }}'" class="block truncate"></span>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<x-ri-expand-up-down-line class="size-4" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
x-show="selectOpen"
|
||||
x-ref="selectableItemsList"
|
||||
@click.away="selectOpen = false"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
:class="{ 'bottom-full mb-1': selectDropdownPosition === 'top', 'top-full mt-1': selectDropdownPosition === 'bottom' }"
|
||||
class="absolute z-50 w-full py-1 overflow-auto bg-background-secondary border border-neutral rounded-lg shadow-lg max-h-60 focus:outline-none text-sm"
|
||||
x-cloak
|
||||
>
|
||||
<template x-for="item in selectableItems" :key="item.value">
|
||||
<li
|
||||
:id="item.value + '-' + selectId"
|
||||
@click="selectedValue = item.value; selectOpen = false"
|
||||
@mousemove="selectableItemActive = item"
|
||||
:class="{ 'bg-neutral-700': selectableItemIsActive(item) }"
|
||||
class="relative py-2 pl-8 pr-4 cursor-pointer select-none hover:bg-neutral-700"
|
||||
>
|
||||
<span
|
||||
class="block truncate"
|
||||
:class="{ 'font-semibold': item.value === selectedValue }"
|
||||
x-text="item.label"
|
||||
></span>
|
||||
<span
|
||||
x-show="item.value === selectedValue"
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-2 text-neutral-400"
|
||||
>
|
||||
<x-ri-check-fill class="size-4" />
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
47
Disabled/ProductPageBuilder/resources/views/index.blade.php
Normal file
47
Disabled/ProductPageBuilder/resources/views/index.blade.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="title">Games</x-slot>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
@foreach($pages as $page)
|
||||
<div class="group relative bg-background-secondary hover:bg-background-secondary/80 border border-neutral rounded-lg overflow-hidden transition-all duration-300 hover:shadow-xl p-4">
|
||||
@if(isset($page->hero_section['background']))
|
||||
<div class="w-full h-36 overflow-hidden rounded-lg mb-4">
|
||||
<img src="{{ Storage::url($page->hero_section['background']) }}"
|
||||
alt="{{ $page->hero_section['title'] }}"
|
||||
class="w-full h-full object-cover">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="relative">
|
||||
<h2 class="text-2xl font-bold text-base mb-4">{{ $page->hero_section['title'] }}</h2>
|
||||
@if(isset($page->hero_section['description']))
|
||||
<div class="prose dark:prose-invert line-clamp-2 mb-4">
|
||||
{!! strip_tags($page->hero_section['description']) !!}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
@if($page->min_price_details)
|
||||
<div class="text-base text-white font-semibold">
|
||||
@php
|
||||
$currency = \App\Models\Currency::firstWhere('code', $page->min_price_details['currency_code']);
|
||||
$formattedPrice = $currency ?
|
||||
($currency->prefix ?? '') . number_format($page->min_price_details['price'], 2) . ($currency->suffix ?? '') :
|
||||
number_format($page->min_price_details['price'], 2);
|
||||
@endphp
|
||||
Starting from {{ $formattedPrice }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-end">
|
||||
<a href="{{ route('product-page.show', $page->hero_section['slug']) }}" wire:navigate class="flex items-center gap-2 justify-center bg-primary text-white text-sm font-semibold hover:bg-primary/80 py-2.5 lg:py-2 px-4.5 rounded-md w-full duration-300 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50">
|
||||
View Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-app-layout>
|
||||
273
Disabled/ProductPageBuilder/resources/views/show.blade.php
Normal file
273
Disabled/ProductPageBuilder/resources/views/show.blade.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="title">{{ $page->seo['title'] ?? $page->hero_section['title'] }}</x-slot>
|
||||
<x-slot name="description">{!! $page->seo['description'] ?? strip_tags($page->hero_section['description']) !!}</x-slot>
|
||||
{{--
|
||||
<x-slot name="keywords">{{ is_array($page->seo['keywords'] ?? null) ? implode(', ', $page->seo['keywords']) : ($page->seo['keywords'] ?? '') }}</x-slot>
|
||||
--}}
|
||||
@if(isset($page->seo['banner']))
|
||||
<x-slot name="image">{{ Storage::url($page->seo['banner']) }}</x-slot>
|
||||
@endif
|
||||
|
||||
<!-- Main content -->
|
||||
@if(isset($page->hero_section))
|
||||
<div class="relative bg-background-secondary hover:bg-background-secondary/80 border border-neutral p-4 rounded-lg flex items-center overflow-hidden">
|
||||
@if(isset($page->hero_section['background']))
|
||||
<div class="absolute inset-0 transform">
|
||||
<img src="{{ Storage::url($page->hero_section['background']) }}"
|
||||
alt="{{ $page->hero_section['title'] }}"
|
||||
class="h-full w-full object-cover">
|
||||
<div class="absolute inset-0 bg-background-secondary/50 dark:bg-background-secondary/60"></div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="relative w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 lg:py-28">
|
||||
<h1 class="text-4xl lg:text-5xl font-extrabold text-base mb-4 leading-tight">
|
||||
{{ $page->hero_section['title'] }}
|
||||
</h1>
|
||||
<div class="text-xl prose dark:prose-invert max-w-5xl">
|
||||
{!! $page->hero_section['description'] !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($page->category)
|
||||
<div class="py-24">
|
||||
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach($page->category->products()->with(['plans.prices'])->get() as $product)
|
||||
@php
|
||||
$lowestPlan = $product->plans
|
||||
->flatMap(function($plan) {
|
||||
return $plan->prices->map(function($price) use ($plan) {
|
||||
return ['plan' => $plan, 'price' => $price];
|
||||
});
|
||||
})
|
||||
->sortBy('price.price')
|
||||
->first();
|
||||
@endphp
|
||||
<div class="bg-background-secondary hover:bg-background-secondary/80 border border-neutral p-4 lg:p-6 rounded-lg overflow-hidden transition-all duration-300 hover:shadow-xl">
|
||||
{{-- Prdocut Image Banner
|
||||
@if($product->image)
|
||||
<div class="mb-4 rounded-lg overflow-hidden">
|
||||
<img src="{{ Storage::url($product->image) }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="w-full h-48 object-cover">
|
||||
</div>
|
||||
@endif
|
||||
--}}
|
||||
{{-- Product Image Icon --}}
|
||||
@if($product->image)
|
||||
<div class="mb-4 rounded-lg overflow-hidden">
|
||||
<img src="{{ Storage::url($product->image) }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="w-full h-24 object-contain">
|
||||
</div>
|
||||
@endif
|
||||
<h3 class="text-3xl font-bold text-base my-4 text-center">
|
||||
{{ $product->name }}
|
||||
</h3>
|
||||
<div class="prose dark:prose-invert text-base mb-4">{!! $product->description !!}</div>
|
||||
@if($lowestPlan)
|
||||
@php
|
||||
$currency = \App\Models\Currency::firstWhere('code', $lowestPlan['price']->currency_code);
|
||||
$formattedPrice = $currency ?
|
||||
($currency->prefix ?? '') . number_format($lowestPlan['price']->price, 2) . ($currency->suffix ?? '') :
|
||||
number_format($lowestPlan['price']->price, 2);
|
||||
@endphp
|
||||
<div class="prose dark:prose-invert mb-4 text-center flex items-center gap-2 justify-center">
|
||||
<span class="font-medium text-lg">Starting from</span>
|
||||
<span class="text-base text-2xl font-bold">{{ $formattedPrice }}</span>
|
||||
</div>
|
||||
@endif
|
||||
<a href="{{ route('products.show', ['category' => $page->category->slug, 'product' => $product->slug]) }}"
|
||||
wire:navigate
|
||||
class="flex items-center gap-2 justify-center bg-primary text-base text-sm font-semibold hover:bg-primary/80 py-2.5 lg:py-2 px-4.5 rounded-md w-full duration-300 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50">
|
||||
{{ __('general.view') }}
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$activeFeatures = collect($page->features ?? [])->filter(fn($feature) => $feature['is_active'] ?? true);
|
||||
@endphp
|
||||
|
||||
@if($activeFeatures->isNotEmpty())
|
||||
<div class="py-12 lg:py-24">
|
||||
<h2 class="text-3xl font-bold text-center text-base mb-12">Features</h2>
|
||||
<div class="grid grid-cols-1 gap-12 lg:grid-cols-3">
|
||||
@foreach($activeFeatures as $feature)
|
||||
<div class="relative group">
|
||||
<div class="relative bg-background-secondary hover:bg-background-secondary/80 border border-neutral p-4 lg:p-6 rounded-lg shadow-xl">
|
||||
@if(!empty($feature['icon_svg']))
|
||||
<div class="h-12 w-12 mb-4 flex items-center justify-center">
|
||||
{!! $feature['icon_svg'] !!}
|
||||
</div>
|
||||
@elseif(isset($feature['icon']))
|
||||
<div class="h-12 w-12 mb-4">
|
||||
<img src="{{ Storage::url($feature['icon']) }}"
|
||||
alt="{{ $feature['title'] }}"
|
||||
class="h-full w-full object-contain">
|
||||
</div>
|
||||
@endif
|
||||
<h3 class="text-xl font-semibold text-base mb-3">{{ $feature['title'] }}</h3>
|
||||
<div class="prose prose dark:prose-invert">{!! $feature['description'] !!}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@if(!empty($page->game_preview))
|
||||
@foreach($page->game_preview as $preview)
|
||||
@if($preview['is_active'] ?? true)
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center py-12 lg:py-24">
|
||||
<div class="max-w-xl me-auto lg:text-left text-center">
|
||||
<h3 class="text-3xl font-bold text-base mb-6">{{ $preview['title'] }}</h3>
|
||||
<div class="prose dark:prose-invert text-base">{!! $preview['description'] !!}</div>
|
||||
</div>
|
||||
@if(isset($preview['youtube_embed']))
|
||||
<div class="h-48 lg:h-96 rounded-lg overflow-hidden">
|
||||
<iframe src="{{ $preview['youtube_embed'] }}"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
class="w-full h-full"></iframe>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@php
|
||||
$activeGamePanels = collect($page->game_panel ?? [])->filter(fn($panel) => $panel['is_active'] ?? true);
|
||||
$activeGamePreviews = collect($page->game_preview ?? [])->filter(fn($preview) => $preview['is_active'] ?? true);
|
||||
$swapOrder = $activeGamePanels->isNotEmpty() && $activeGamePreviews->isNotEmpty();
|
||||
@endphp
|
||||
|
||||
@if(!empty($page->game_panel))
|
||||
@foreach($page->game_panel as $panel)
|
||||
@if($panel['is_active'] ?? true)
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center py-12 lg:py-24">
|
||||
<div class="max-w-xl ms-auto lg:text-left text-center {{ $swapOrder ? 'order-2' : 'order-1' }}">
|
||||
<h3 class="text-3xl font-bold text-base mb-6">{{ $panel['title'] }}</h3>
|
||||
<div class="prose dark:prose-invert text-base">{!! $panel['description'] !!}</div>
|
||||
@php
|
||||
$features = is_array($panel['features'] ?? null)
|
||||
? $panel['features']
|
||||
: (empty($panel['features']) ? [] : explode(',', $panel['features']));
|
||||
@endphp
|
||||
@if(!empty($features))
|
||||
<div class="flex flex-wrap justify-center lg:justify-start gap-2 my-4">
|
||||
@foreach($features as $feature)
|
||||
<span class="flex items-center gap-1 bg-background-secondary border border-neutral text-base px-3 py-1 rounded-lg text-sm font-semibold">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 text-primary">
|
||||
<path d="M9.9997 15.1709L19.1921 5.97852L20.6063 7.39273L9.9997 17.9993L3.63574 11.6354L5.04996 10.2212L9.9997 15.1709Z"></path>
|
||||
</svg>
|
||||
{{ trim($feature) }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@if(!empty($panel['gallery']))
|
||||
<div
|
||||
x-data="{
|
||||
slides: {{ json_encode($panel['gallery']) }},
|
||||
current: 0,
|
||||
goTo(idx) { this.current = idx },
|
||||
next() { this.current = (this.current + 1) % this.slides.length },
|
||||
prev() { this.current = (this.current - 1 + this.slides.length) % this.slides.length }
|
||||
}"
|
||||
class="relative rounded-lg overflow-hidden shadow-lg"
|
||||
>
|
||||
<div class="relative h-48 md:h-96">
|
||||
<template x-for="(image, idx) in slides" :key="idx">
|
||||
<div
|
||||
x-show="current === idx"
|
||||
x-transition:enter="transition-opacity duration-700"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition-opacity duration-700"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0"
|
||||
>
|
||||
<img :src="'{{ Storage::url('') }}' + image" class="object-cover w-full h-full" alt="Slide" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Slider indicators -->
|
||||
<div class="flex absolute bottom-5 left-1/2 z-30 -translate-x-1/2 space-x-2">
|
||||
<template x-for="(image, idx) in slides" :key="idx">
|
||||
<button
|
||||
type="button"
|
||||
:class="{'bg-primary': current === idx, 'bg-gray-300': current !== idx}"
|
||||
class="w-4 h-1 rounded-lg hover:bg-gray-400 focus:outline-none transition"
|
||||
@click="goTo(idx)"
|
||||
></button>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Slider controls -->
|
||||
<button type="button"
|
||||
class="flex absolute top-1/2 -translate-y-1/2 left-3 z-40 items-center justify-center w-8 h-8 bg-gray-200/50 rounded-full hover:bg-gray-300 focus:outline-none transition cursor-pointer"
|
||||
@click="prev"
|
||||
>
|
||||
<svg class="w-4 h-4 text-gray-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="flex absolute top-1/2 -translate-y-1/2 right-3 z-40 items-center justify-center w-8 h-8 bg-gray-200/50 rounded-full hover:bg-gray-300 focus:outline-none transition cursor-pointer"
|
||||
@click="next"
|
||||
>
|
||||
<svg class="w-4 h-4 text-gray-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@php
|
||||
$activeFaqs = collect($page->faqs ?? [])->filter(fn($faq) => $faq['is_active'] ?? true);
|
||||
@endphp
|
||||
|
||||
@if($activeFaqs->isNotEmpty())
|
||||
<div class="max-w-7xl mx-auto py-12 lg:py-24">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-3xl font-bold text-center text-base mb-12">Frequently Asked Questions</h2>
|
||||
<div class="space-y-8">
|
||||
@foreach($activeFaqs as $faq)
|
||||
<div class="bg-background-secondary hover:bg-background-secondary/80 border border-neutral p-4 rounded-lg" x-data="{ open: false }">
|
||||
<dt>
|
||||
<button @click="open = !open" class="w-full flex justify-between items-center text-left cursor-pointer">
|
||||
<span class="text-base lg:text-lg font-semibold text-base">{{ $faq['question'] }}</span>
|
||||
<span class="ml-6 flex-shrink-0">
|
||||
<svg class="h-4 w-4 transform transition-transform duration-200" :class="{'rotate-180': open}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</dt>
|
||||
<dd class="mt-4" x-show="open" x-transition>
|
||||
<div class="prose prose-sm max-w-none prose dark:prose-invert">
|
||||
{!! $faq['answer'] !!}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</x-app-layout>
|
||||
7
Disabled/ProductPageBuilder/routes/web.php
Normal file
7
Disabled/ProductPageBuilder/routes/web.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Paymenter\Extensions\Others\ProductPageBuilder\Http\Controllers\ProductPageController;
|
||||
|
||||
Route::get('/games', [ProductPageController::class, 'index'])->name('product-page.index');
|
||||
Route::get('/games/{slug}', [ProductPageController::class, 'show'])->name('product-page.show');
|
||||
Reference in New Issue
Block a user