Onboarding Upload
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Onboarding\Admin\Resources;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource\Pages\CreateOnboardingSlide;
|
||||
use Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource\Pages\EditOnboardingSlide;
|
||||
use Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource\Pages\ListOnboardingSlides;
|
||||
use Paymenter\Extensions\Others\Onboarding\Models\OnboardingSlide;
|
||||
|
||||
class OnboardingResource extends Resource
|
||||
{
|
||||
protected static ?string $model = OnboardingSlide::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'ri-slideshow-line';
|
||||
|
||||
protected static string|\BackedEnum|null $activeNavigationIcon = 'ri-slideshow-fill';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Administration';
|
||||
|
||||
protected static ?string $navigationLabel = 'Onboarding';
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->columns(1)
|
||||
->components([
|
||||
TextInput::make('title')
|
||||
->label('Title')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('Welcome to Our Platform'),
|
||||
|
||||
Textarea::make('description')
|
||||
->label('Description')
|
||||
->required()
|
||||
->rows(4)
|
||||
->placeholder('A brief description of this onboarding step...'),
|
||||
|
||||
FileUpload::make('image')
|
||||
->label('Image')
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('onboarding')
|
||||
->visibility('public')
|
||||
->imageEditor()
|
||||
->imageEditorAspectRatios([
|
||||
'16:9',
|
||||
'4:3',
|
||||
'1:1',
|
||||
])
|
||||
->maxSize(2048)
|
||||
->helperText('Upload an image for this slide (max 2MB). Recommended aspect ratio: 16:9'),
|
||||
|
||||
TextInput::make('sort')
|
||||
->label('Sort Order')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->required()
|
||||
->helperText('Lower numbers appear first'),
|
||||
|
||||
Toggle::make('enabled')
|
||||
->label('Enabled')
|
||||
->default(true)
|
||||
->helperText('Only enabled slides will be shown to users'),
|
||||
|
||||
Toggle::make('is_guided')
|
||||
->label('Guided Slide')
|
||||
->default(false)
|
||||
->helperText('If enabled, this slide will redirect the user to a specific page')
|
||||
->live(),
|
||||
|
||||
Select::make('page_route')
|
||||
->label('Page to Guide To')
|
||||
->visible(fn (Get $get): bool => $get('is_guided'))
|
||||
->required(fn (Get $get): bool => $get('is_guided'))
|
||||
->options([
|
||||
'dashboard' => 'Dashboard',
|
||||
'services' => 'Services',
|
||||
'services.first' => 'First Active Service',
|
||||
'invoices' => 'Invoices',
|
||||
'tickets' => 'Tickets',
|
||||
'tickets.create' => 'Create Ticket',
|
||||
'account' => 'Account Settings',
|
||||
'account.security' => 'Account Security',
|
||||
'account.payment-methods' => 'Payment Methods',
|
||||
'account.notifications' => 'Notifications',
|
||||
'products' => 'Browse Products',
|
||||
])
|
||||
->helperText('Select which page the user should be guided to'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
ImageColumn::make('image')
|
||||
->label('Image')
|
||||
->disk('public')
|
||||
->square()
|
||||
->defaultImageUrl(url('/images/placeholder.png')),
|
||||
|
||||
TextColumn::make('title')
|
||||
->label('Title')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(50),
|
||||
|
||||
TextColumn::make('description')
|
||||
->label('Description')
|
||||
->limit(80)
|
||||
->wrap(),
|
||||
|
||||
TextColumn::make('sort')
|
||||
->label('Order')
|
||||
->sortable()
|
||||
->alignCenter(),
|
||||
|
||||
ToggleColumn::make('enabled')
|
||||
->label('Enabled')
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('is_guided')
|
||||
->label('Guided')
|
||||
->badge()
|
||||
->color(fn (bool $state): string => $state ? 'success' : 'gray')
|
||||
->formatStateUsing(fn (bool $state): string => $state ? 'Yes' : 'No')
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label('Created')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->defaultSort('sort', 'asc')
|
||||
->reorderable('sort');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListOnboardingSlides::route('/'),
|
||||
'create' => CreateOnboardingSlide::route('/create'),
|
||||
'edit' => EditOnboardingSlide::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource\Pages;
|
||||
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource;
|
||||
|
||||
class CreateOnboardingSlide extends CreateRecord
|
||||
{
|
||||
protected static string $resource = OnboardingResource::class;
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource\Pages;
|
||||
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource;
|
||||
|
||||
class EditOnboardingSlide extends EditRecord
|
||||
{
|
||||
protected static string $resource = OnboardingResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource\Pages;
|
||||
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource;
|
||||
|
||||
class ListOnboardingSlides extends ListRecords
|
||||
{
|
||||
protected static string $resource = OnboardingResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
\Filament\Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
167
extensions/Others/Onboarding/Livewire/Onboarding/Modal.php
Normal file
167
extensions/Others/Onboarding/Livewire/Onboarding/Modal.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Onboarding\Livewire\Onboarding;
|
||||
|
||||
use Livewire\Component;
|
||||
use Paymenter\Extensions\Others\Onboarding\Models\OnboardingSlide;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
|
||||
class Modal extends Component
|
||||
{
|
||||
public $currentSlide = 0;
|
||||
public $slides = [];
|
||||
public $showModal = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
// Check if test mode and per session mode are enabled
|
||||
$extension = ExtensionHelper::getExtension('other', 'Onboarding');
|
||||
$testMode = $extension->config('test_mode', false);
|
||||
$perSession = $extension->config('per_session', false);
|
||||
|
||||
$userId = Auth::id();
|
||||
|
||||
// Check if user has seen onboarding before
|
||||
if ($perSession) {
|
||||
// Use cookie for per-session tracking
|
||||
$hasSeenOnboarding = $testMode ? false : \Illuminate\Support\Facades\Cookie::has('onboarding_completed');
|
||||
$this->currentSlide = (int) \Illuminate\Support\Facades\Cookie::get('onboarding_current_slide', 0);
|
||||
} else {
|
||||
// Use database session for persistent tracking
|
||||
$hasSeenOnboarding = $testMode ? false : session()->get("onboarding_seen_{$userId}", false);
|
||||
$this->currentSlide = session()->get("onboarding_current_slide_{$userId}", 0);
|
||||
}
|
||||
|
||||
// Only show if user hasn't seen it and there are slides
|
||||
$this->slides = OnboardingSlide::where('enabled', true)
|
||||
->orderBy('sort', 'asc')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$this->showModal = !$hasSeenOnboarding && count($this->slides) > 0 && $this->currentSlide < count($this->slides);
|
||||
}
|
||||
|
||||
public function getShowBackdropProperty()
|
||||
{
|
||||
$extension = ExtensionHelper::getExtension('other', 'Onboarding');
|
||||
return $extension->config('show_backdrop', false);
|
||||
}
|
||||
|
||||
public function nextSlide()
|
||||
{
|
||||
$currentSlideData = $this->slides[$this->currentSlide] ?? null;
|
||||
$extension = ExtensionHelper::getExtension('other', 'Onboarding');
|
||||
$perSession = $extension->config('per_session', false);
|
||||
|
||||
// If current slide is guided, redirect to the page
|
||||
if ($currentSlideData && $currentSlideData['is_guided'] && $currentSlideData['page_route']) {
|
||||
// Save current slide progress before redirecting
|
||||
$this->currentSlide++;
|
||||
|
||||
if ($perSession) {
|
||||
\Illuminate\Support\Facades\Cookie::queue('onboarding_current_slide', $this->currentSlide, 60 * 24 * 7); // 7 days
|
||||
} else {
|
||||
$userId = Auth::id();
|
||||
session()->put("onboarding_current_slide_{$userId}", $this->currentSlide);
|
||||
}
|
||||
|
||||
$this->redirectToPage($currentSlideData['page_route']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->currentSlide < count($this->slides) - 1) {
|
||||
$this->currentSlide++;
|
||||
// Save progress
|
||||
if ($perSession) {
|
||||
\Illuminate\Support\Facades\Cookie::queue('onboarding_current_slide', $this->currentSlide, 60 * 24 * 7); // 7 days
|
||||
} else {
|
||||
$userId = Auth::id();
|
||||
session()->put("onboarding_current_slide_{$userId}", $this->currentSlide);
|
||||
}
|
||||
} else {
|
||||
$this->finish();
|
||||
}
|
||||
}
|
||||
|
||||
public function previousSlide()
|
||||
{
|
||||
if ($this->currentSlide > 0) {
|
||||
$this->currentSlide--;
|
||||
// Save progress
|
||||
$extension = ExtensionHelper::getExtension('other', 'Onboarding');
|
||||
$perSession = $extension->config('per_session', false);
|
||||
|
||||
if ($perSession) {
|
||||
\Illuminate\Support\Facades\Cookie::queue('onboarding_current_slide', $this->currentSlide, 60 * 24 * 7); // 7 days
|
||||
} else {
|
||||
$userId = Auth::id();
|
||||
session()->put("onboarding_current_slide_{$userId}", $this->currentSlide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function finish()
|
||||
{
|
||||
// Check if test mode and per session mode are enabled
|
||||
$extension = ExtensionHelper::getExtension('other', 'Onboarding');
|
||||
$testMode = $extension->config('test_mode', false);
|
||||
$perSession = $extension->config('per_session', false);
|
||||
|
||||
if ($perSession) {
|
||||
// Clear current slide progress cookie
|
||||
\Illuminate\Support\Facades\Cookie::queue(\Illuminate\Support\Facades\Cookie::forget('onboarding_current_slide'));
|
||||
|
||||
// Only mark as completed if not in test mode (30 days expiration)
|
||||
if (!$testMode) {
|
||||
\Illuminate\Support\Facades\Cookie::queue('onboarding_completed', '1', 60 * 24 * 30);
|
||||
}
|
||||
} else {
|
||||
$userId = Auth::id();
|
||||
|
||||
// Clear current slide progress
|
||||
session()->forget("onboarding_current_slide_{$userId}");
|
||||
|
||||
// Only mark as seen if not in test mode
|
||||
if (!$testMode) {
|
||||
session()->put("onboarding_seen_{$userId}", true);
|
||||
}
|
||||
}
|
||||
|
||||
$this->showModal = false;
|
||||
}
|
||||
|
||||
protected function redirectToPage($pageRoute)
|
||||
{
|
||||
$url = match ($pageRoute) {
|
||||
'dashboard' => route('dashboard'),
|
||||
'services' => route('services'),
|
||||
'services.first' => $this->getFirstServiceUrl(),
|
||||
'invoices' => route('invoices'),
|
||||
'tickets' => route('tickets'),
|
||||
'tickets.create' => route('tickets.create'),
|
||||
'account' => route('account'),
|
||||
'account.security' => route('account.security'),
|
||||
'account.payment-methods' => route('account.payment-methods'),
|
||||
'account.notifications' => route('account.notifications'),
|
||||
'products' => route('home') . '#shop',
|
||||
default => route('dashboard'),
|
||||
};
|
||||
|
||||
if ($url) {
|
||||
return $this->redirect($url, navigate: true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFirstServiceUrl()
|
||||
{
|
||||
$service = Auth::user()->services()->where('status', 'active')->first();
|
||||
return $service ? route('services.show', $service) : route('services');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('onboarding::modal');
|
||||
}
|
||||
}
|
||||
26
extensions/Others/Onboarding/Models/OnboardingSlide.php
Normal file
26
extensions/Others/Onboarding/Models/OnboardingSlide.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Onboarding\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OnboardingSlide extends Model
|
||||
{
|
||||
protected $table = 'ext_onboarding_slides';
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'description',
|
||||
'image',
|
||||
'sort',
|
||||
'enabled',
|
||||
'is_guided',
|
||||
'page_route',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'enabled' => 'boolean',
|
||||
'is_guided' => 'boolean',
|
||||
'sort' => 'integer',
|
||||
];
|
||||
}
|
||||
111
extensions/Others/Onboarding/Onboarding.php
Normal file
111
extensions/Others/Onboarding/Onboarding.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Onboarding;
|
||||
|
||||
use App\Classes\Extension\Extension;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
|
||||
class Onboarding extends Extension
|
||||
{
|
||||
public function getMetadata()
|
||||
{
|
||||
return [
|
||||
'display_name' => 'Onboarding',
|
||||
'version' => '1.0.0',
|
||||
'author' => 'Paymenter',
|
||||
'website' => 'https://paymenter.org',
|
||||
];
|
||||
}
|
||||
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'test_mode',
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Test Mode',
|
||||
'description' => 'Enable test mode to always show onboarding modal (bypass session tracking). Useful for testing while setting up slides.',
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'show_backdrop',
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Show Backdrop',
|
||||
'description' => 'Display a dark backdrop behind the onboarding modal.',
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'per_session',
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Per Session Onboarding',
|
||||
'description' => 'Use cookies instead of database session. Onboarding will show again if user clears cookies or uses a different browser/device.',
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function installed()
|
||||
{
|
||||
|
||||
ExtensionHelper::runMigrations('extensions/Others/Onboarding/database/migrations');
|
||||
}
|
||||
|
||||
public function uninstalled()
|
||||
{
|
||||
|
||||
ExtensionHelper::rollbackMigrations('extensions/Others/Onboarding/database/migrations');
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
|
||||
if (app()->runningInConsole() === false) {
|
||||
\Filament\Facades\Filament::getCurrentPanel()?->resources([
|
||||
\Paymenter\Extensions\Others\Onboarding\Admin\Resources\OnboardingResource::class,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
app('view')->addNamespace('onboarding', __DIR__ . '/resources/views');
|
||||
|
||||
|
||||
\Livewire\Livewire::component('onboarding.modal', \Paymenter\Extensions\Others\Onboarding\Livewire\Onboarding\Modal::class);
|
||||
|
||||
|
||||
\Illuminate\Support\Facades\Gate::policy(
|
||||
\Paymenter\Extensions\Others\Onboarding\Models\OnboardingSlide::class,
|
||||
\Paymenter\Extensions\Others\Onboarding\Policies\OnboardingSlidePolicy::class
|
||||
);
|
||||
|
||||
|
||||
\Illuminate\Support\Facades\Event::listen('permissions', function () {
|
||||
return [
|
||||
'admin.onboarding.view' => 'View Onboarding Slides',
|
||||
'admin.onboarding.create' => 'Create Onboarding Slides',
|
||||
'admin.onboarding.update' => 'Update Onboarding Slides',
|
||||
'admin.onboarding.delete' => 'Delete Onboarding Slides',
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
\Illuminate\Support\Facades\Event::listen('body', function () {
|
||||
|
||||
if (!request()->is('admin/*') &&
|
||||
!request()->is('login') &&
|
||||
!request()->is('register') &&
|
||||
!request()->is('2fa') &&
|
||||
!request()->is('forgot-password') &&
|
||||
!request()->is('reset-password/*') &&
|
||||
!request()->is('verify-email') &&
|
||||
!request()->is('/') &&
|
||||
\Illuminate\Support\Facades\Auth::check()) {
|
||||
return [
|
||||
'view' => view('onboarding::widget'),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Onboarding\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Policies\BasePolicy;
|
||||
use Paymenter\Extensions\Others\Onboarding\Models\OnboardingSlide;
|
||||
|
||||
class OnboardingSlidePolicy extends BasePolicy
|
||||
{
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->hasPermission('admin.onboarding.view');
|
||||
}
|
||||
|
||||
|
||||
public function view(User $user, OnboardingSlide $onboardingSlide): bool
|
||||
{
|
||||
return $user->hasPermission('admin.onboarding.view');
|
||||
}
|
||||
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->hasPermission('admin.onboarding.create');
|
||||
}
|
||||
|
||||
|
||||
public function update(User $user, OnboardingSlide $onboardingSlide): bool
|
||||
{
|
||||
return $user->hasPermission('admin.onboarding.update');
|
||||
}
|
||||
|
||||
|
||||
public function delete(User $user, OnboardingSlide $onboardingSlide): bool
|
||||
{
|
||||
return $user->hasPermission('admin.onboarding.delete');
|
||||
}
|
||||
|
||||
|
||||
public function deleteAny(User $user): bool
|
||||
{
|
||||
return $user->hasPermission('admin.onboarding.delete');
|
||||
}
|
||||
}
|
||||
16
extensions/Others/Onboarding/composer.json
Normal file
16
extensions/Others/Onboarding/composer.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "paymenter/onboarding",
|
||||
"description": "Onboarding extension for Paymenter",
|
||||
"type": "paymenter-extension",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Paymenter\\Extensions\\Others\\Onboarding\\": ""
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"paymenter": {
|
||||
"extension": "Onboarding",
|
||||
"type": "other"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ext_onboarding_slides', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title');
|
||||
$table->text('description');
|
||||
$table->string('image')->nullable();
|
||||
$table->integer('sort')->default(0);
|
||||
$table->boolean('enabled')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ext_onboarding_slides');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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::table('ext_onboarding_slides', function (Blueprint $table) {
|
||||
$table->boolean('is_guided')->default(false)->after('enabled');
|
||||
$table->string('page_route')->nullable()->after('is_guided');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('ext_onboarding_slides', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_guided', 'page_route']);
|
||||
});
|
||||
}
|
||||
};
|
||||
104
extensions/Others/Onboarding/resources/views/modal.blade.php
Normal file
104
extensions/Others/Onboarding/resources/views/modal.blade.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<div>
|
||||
@if($showModal && count($slides) > 0)
|
||||
<div x-data="{ open: @entangle('showModal'), currentSlide: @entangle('currentSlide') }"
|
||||
x-show="open"
|
||||
x-cloak
|
||||
x-init="$watch('open', value => {
|
||||
if(value) {
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.documentElement.style.overflow = '';
|
||||
}
|
||||
})"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center overflow-hidden">
|
||||
|
||||
|
||||
@if($this->showBackdrop)
|
||||
<div class="absolute inset-0 bg-black/50 transition-opacity duration-300"
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"></div>
|
||||
@endif
|
||||
|
||||
|
||||
<div class="w-full max-w-xl mx-4 md:mx-auto bg-background-secondary border border-neutral rounded-lg shadow-2xl overflow-hidden h-[500px] flex flex-col relative z-10"
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90">
|
||||
|
||||
|
||||
@foreach($slides as $index => $slide)
|
||||
<div wire:key="slide-{{ $slide['id'] }}"
|
||||
x-show="currentSlide === {{ $index }}"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="flex flex-col h-full absolute inset-0">
|
||||
|
||||
|
||||
@if($slide['image'])
|
||||
<div class="w-full h-56 flex-shrink-0 overflow-hidden rounded-t-lg">
|
||||
<img class="w-full h-full object-cover"
|
||||
src="{{ Storage::url($slide['image']) }}"
|
||||
alt="{{ $slide['title'] }}">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
<div class="p-6 flex-1 overflow-y-auto">
|
||||
<h3 class="text-xl font-bold text-primary-100 mb-2.5">
|
||||
{{ $slide['title'] }}
|
||||
</h3>
|
||||
<p class="text-sm text-base/80 leading-relaxed">
|
||||
{{ $slide['description'] }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-center gap-2 px-6 pb-4 flex-shrink-0">
|
||||
@foreach($slides as $dotIndex => $dot)
|
||||
<div class="h-1.5 rounded-full transition-all duration-300"
|
||||
:class="currentSlide === {{ $dotIndex }} ? 'w-6 bg-primary' : 'w-1.5 bg-neutral'"></div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-between items-center px-6 pb-5 pt-1 flex-shrink-0">
|
||||
<button wire:click="previousSlide"
|
||||
:disabled="currentSlide === 0"
|
||||
class="px-4 py-1.5 text-sm text-base/70 hover:text-primary-100 font-medium transition disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Back
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button @click="currentSlide === {{ count($slides) - 1 }} ? $wire.finish() : $wire.nextSlide()"
|
||||
class="px-5 py-1.5 text-sm bg-primary hover:bg-primary/90 text-white font-medium rounded-md transition shadow-sm">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span x-text="currentSlide === {{ count($slides) - 1 }} ? 'Finish' : (@js($slide['is_guided'] ?? false) ? 'Continue' : 'Next')"></span>
|
||||
<svg x-show="currentSlide !== {{ count($slides) - 1 }}" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<livewire:onboarding.modal />
|
||||
Reference in New Issue
Block a user