From ed7055edaa3ad1efcbc57397d6bc678721a70d35 Mon Sep 17 00:00:00 2001 From: theRADcozaDEV Date: Fri, 13 Mar 2026 14:21:43 +0200 Subject: [PATCH] feat: Add frontend menu management system with permission filtering --- AI_CONTEXT.md | 15 ++ TEMPLATE_FIX_SUGGESTIONS.md | 16 ++ docs/menu-management.md | 130 +++++++++++++++ src/app/Filament/Resources/MenuResource.php | 146 +++++++++++++++++ .../MenuResource/Pages/CreateMenu.php | 11 ++ .../Resources/MenuResource/Pages/EditMenu.php | 19 +++ .../MenuResource/Pages/ListMenus.php | 19 +++ .../RelationManagers/ItemsRelationManager.php | 153 ++++++++++++++++++ src/app/Models/Menu.php | 40 +++++ src/app/Models/MenuItem.php | 90 +++++++++++ src/app/Services/MenuService.php | 117 ++++++++++++++ src/app/View/Components/FrontendMenu.php | 30 ++++ .../Components/FrontendMenuResponsive.php | 30 ++++ .../2024_01_01_000001_create_menus_table.php | 25 +++ ...4_01_01_000002_create_menu_items_table.php | 34 ++++ src/database/seeders/DatabaseSeeder.php | 1 + src/database/seeders/MenuSeeder.php | 33 ++++ src/database/seeders/RolePermissionSeeder.php | 4 + .../frontend-menu-responsive.blade.php | 47 ++++++ .../views/components/frontend-menu.blade.php | 60 +++++++ .../views/layouts/navigation.blade.php | 10 +- 21 files changed, 1023 insertions(+), 7 deletions(-) create mode 100644 TEMPLATE_FIX_SUGGESTIONS.md create mode 100644 docs/menu-management.md create mode 100644 src/app/Filament/Resources/MenuResource.php create mode 100644 src/app/Filament/Resources/MenuResource/Pages/CreateMenu.php create mode 100644 src/app/Filament/Resources/MenuResource/Pages/EditMenu.php create mode 100644 src/app/Filament/Resources/MenuResource/Pages/ListMenus.php create mode 100644 src/app/Filament/Resources/MenuResource/RelationManagers/ItemsRelationManager.php create mode 100644 src/app/Models/Menu.php create mode 100644 src/app/Models/MenuItem.php create mode 100644 src/app/Services/MenuService.php create mode 100644 src/app/View/Components/FrontendMenu.php create mode 100644 src/app/View/Components/FrontendMenuResponsive.php create mode 100644 src/database/migrations/2024_01_01_000001_create_menus_table.php create mode 100644 src/database/migrations/2024_01_01_000002_create_menu_items_table.php create mode 100644 src/database/seeders/MenuSeeder.php create mode 100644 src/resources/views/components/frontend-menu-responsive.blade.php create mode 100644 src/resources/views/components/frontend-menu.blade.php diff --git a/AI_CONTEXT.md b/AI_CONTEXT.md index 3fb3a61..2cd79e3 100644 --- a/AI_CONTEXT.md +++ b/AI_CONTEXT.md @@ -5,6 +5,21 @@ This document provides context for AI coding assistants working on projects built with this template. +## Development Workflow Rules + +### Git Branching Strategy + +**Rule**: The AI agent must create a new Git branch for every new feature, module, or significant change. It should never commit or push directly to the `main` branch. + +**Rationale**: This practice is crucial for maintaining a clean and stable main branch, facilitating code reviews, and making it easier for human developers to collaborate and manage the project's history. + +**Example Workflow**: +1. `git checkout -b feature/new-company-module` +2. *...perform all work related to the new module...* +3. `git add .` +4. `git commit -m "feat: Create new company module"` +5. *...agent informs the user that the feature is complete on the new branch...* + ## Template Overview This is a **ready-to-use Laravel Docker Development Template** with everything pre-installed: diff --git a/TEMPLATE_FIX_SUGGESTIONS.md b/TEMPLATE_FIX_SUGGESTIONS.md new file mode 100644 index 0000000..f07925f --- /dev/null +++ b/TEMPLATE_FIX_SUGGESTIONS.md @@ -0,0 +1,16 @@ +# Template Improvement Suggestions + +This document contains suggestions for improving the template based on recurring development issues. + +## 1. Mandatory Module Settings Page + +**Rule**: When creating a new module, the AI agent must always create a corresponding settings page within the admin panel. + +**Minimum Requirements**: This settings page must, at a minimum, allow an administrator to configure which user roles have Create, Read, Update, and Delete (CRUD) permissions for that module. + +**Rationale**: This has been a recurring issue. Automating the creation of a permission management UI for each module makes the template more robust, secure, and user-friendly out-of-the-box. It prevents situations where new modules are added without any way for an admin to control access to them. + +**Example Implementation**: +- When a `Blog` module is created, a `Blog Settings` page should also be created. +- This page should contain a form with checkboxes or a multi-select dropdown for each CRUD permission (`blog.view`, `blog.create`, `blog.edit`, `blog.delete`). +- An administrator can then select which roles (e.g., 'admin', 'editor', 'viewer') are granted each of these permissions. diff --git a/docs/menu-management.md b/docs/menu-management.md new file mode 100644 index 0000000..8e42238 --- /dev/null +++ b/docs/menu-management.md @@ -0,0 +1,130 @@ +# Frontend Menu Management + +This template includes a dynamic menu management system that allows administrators to create and manage frontend navigation menus through the admin panel. + +## Features + +- **Dynamic Menus**: Create multiple menus (header, footer, sidebar, etc.) +- **Nested Items**: Support for parent-child menu items (dropdowns) +- **Multiple Link Types**: + - External links (URLs) + - Named routes + - Module links (auto-generates route from module slug) +- **Permission-Based Filtering**: Menu items are automatically filtered based on user permissions +- **Drag & Drop Reordering**: Reorder menu items in the admin panel + +## How It Works + +### Permission Filtering + +Menu items are automatically filtered based on the current user's permissions: + +1. **Module Items**: If a menu item links to a module (e.g., `companies`), the user must have `{module}.view` permission (e.g., `companies.view`) to see that item. + +2. **Permission Items**: If a menu item has a specific `permission` set, the user must have that exact permission. + +3. **Admin Users**: Users with the `admin` role can see all menu items. + +4. **Guest Users**: Only see items with no permission or module restrictions. + +## Usage + +### In Blade Templates + +Use the `` component to render a menu: + +```blade +{{-- Desktop navigation --}} + + +{{-- Mobile/responsive navigation --}} + + +{{-- By location --}} + +``` + +### Programmatic Access + +```php +use App\Services\MenuService; + +$menuService = app(MenuService::class); + +// Get menu items (already filtered for current user) +$items = $menuService->getMenu('header'); + +// Get available modules for menu items +$modules = $menuService->getAvailableModules(); + +// Get available routes for menu items +$routes = $menuService->getAvailableRoutes(); +``` + +## Admin Panel + +Navigate to **Settings > Menus** in the admin panel to: + +1. Create new menus +2. Add/edit/delete menu items +3. Set item types (link, route, module) +4. Configure permissions for each item +5. Reorder items via drag & drop + +## Database Structure + +### `menus` table +- `id` - Primary key +- `name` - Display name +- `slug` - Unique identifier (used in templates) +- `location` - Optional location hint (header, footer, sidebar) +- `is_active` - Toggle menu visibility + +### `menu_items` table +- `id` - Primary key +- `menu_id` - Foreign key to menus +- `parent_id` - For nested items +- `title` - Display text +- `type` - link, route, or module +- `url` - For external links +- `route` - For named routes +- `route_params` - JSON parameters for routes +- `module` - Module slug for permission checking +- `permission` - Specific permission required +- `icon` - Heroicon name +- `target` - _self or _blank +- `order` - Sort order +- `is_active` - Toggle visibility + +## Adding Menu Items for New Modules + +When creating a new module, add a menu item to link to it: + +1. Go to **Settings > Menus** in admin +2. Edit the "Header Navigation" menu +3. Click "New" in the Items section +4. Set: + - **Title**: Your module name + - **Type**: Module + - **Module**: Select your module from dropdown +5. Save + +The menu item will automatically: +- Generate the correct URL (`/{module-slug}`) +- Check for `{module}.view` permission +- Hide from users without permission + +## Customization + +### Custom Menu Component + +Copy and modify the default component: + +```bash +cp resources/views/components/frontend-menu.blade.php \ + resources/views/components/my-custom-menu.blade.php +``` + +### Styling + +The default components use Tailwind CSS classes. Modify the component templates to match your design. diff --git a/src/app/Filament/Resources/MenuResource.php b/src/app/Filament/Resources/MenuResource.php new file mode 100644 index 0000000..0008a25 --- /dev/null +++ b/src/app/Filament/Resources/MenuResource.php @@ -0,0 +1,146 @@ +user(); + return $user?->hasRole('admin') || $user?->can('menus.view'); + } + + public static function canCreate(): bool + { + $user = auth()->user(); + return $user?->hasRole('admin') || $user?->can('menus.create'); + } + + public static function canEdit($record): bool + { + $user = auth()->user(); + return $user?->hasRole('admin') || $user?->can('menus.edit'); + } + + public static function canDelete($record): bool + { + $user = auth()->user(); + return $user?->hasRole('admin') || $user?->can('menus.delete'); + } + + public static function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Section::make('Menu Details') + ->schema([ + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(255) + ->live(onBlur: true) + ->afterStateUpdated(fn ($state, Forms\Set $set) => + $set('slug', Str::slug($state)) + ), + Forms\Components\TextInput::make('slug') + ->required() + ->maxLength(255) + ->unique(ignoreRecord: true), + Forms\Components\Select::make('location') + ->options([ + 'header' => 'Header Navigation', + 'footer' => 'Footer Navigation', + 'sidebar' => 'Sidebar Navigation', + ]) + ->placeholder('Select a location'), + Forms\Components\Toggle::make('is_active') + ->label('Active') + ->default(true), + ])->columns(2), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('slug') + ->searchable() + ->badge() + ->color('gray'), + Tables\Columns\TextColumn::make('location') + ->badge() + ->color(fn (?string $state) => match ($state) { + 'header' => 'success', + 'footer' => 'warning', + 'sidebar' => 'info', + default => 'gray', + }), + Tables\Columns\TextColumn::make('allItems_count') + ->label('Items') + ->counts('allItems') + ->badge(), + Tables\Columns\IconColumn::make('is_active') + ->label('Active') + ->boolean(), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('location') + ->options([ + 'header' => 'Header', + 'footer' => 'Footer', + 'sidebar' => 'Sidebar', + ]), + Tables\Filters\TernaryFilter::make('is_active') + ->label('Active'), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + RelationManagers\ItemsRelationManager::class, + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListMenus::route('/'), + 'create' => Pages\CreateMenu::route('/create'), + 'edit' => Pages\EditMenu::route('/{record}/edit'), + ]; + } +} diff --git a/src/app/Filament/Resources/MenuResource/Pages/CreateMenu.php b/src/app/Filament/Resources/MenuResource/Pages/CreateMenu.php new file mode 100644 index 0000000..0a951a0 --- /dev/null +++ b/src/app/Filament/Resources/MenuResource/Pages/CreateMenu.php @@ -0,0 +1,11 @@ +schema([ + Forms\Components\Section::make('Item Details') + ->schema([ + Forms\Components\TextInput::make('title') + ->required() + ->maxLength(255), + Forms\Components\Select::make('type') + ->options([ + 'link' => 'External Link', + 'route' => 'Named Route', + 'module' => 'Module', + ]) + ->default('link') + ->required() + ->live(), + Forms\Components\TextInput::make('url') + ->label('URL') + ->url() + ->visible(fn (Forms\Get $get) => $get('type') === 'link') + ->required(fn (Forms\Get $get) => $get('type') === 'link'), + Forms\Components\Select::make('route') + ->label('Route Name') + ->options(fn () => $menuService->getAvailableRoutes()) + ->searchable() + ->visible(fn (Forms\Get $get) => $get('type') === 'route') + ->required(fn (Forms\Get $get) => $get('type') === 'route'), + Forms\Components\Select::make('module') + ->label('Module') + ->options(fn () => $menuService->getAvailableModules()) + ->searchable() + ->visible(fn (Forms\Get $get) => $get('type') === 'module') + ->required(fn (Forms\Get $get) => $get('type') === 'module') + ->helperText('Users need view permission for this module to see this item'), + ])->columns(2), + + Forms\Components\Section::make('Permissions & Display') + ->schema([ + Forms\Components\Select::make('parent_id') + ->label('Parent Item') + ->options(fn () => MenuItem::where('menu_id', $this->ownerRecord->id) + ->whereNull('parent_id') + ->pluck('title', 'id')) + ->placeholder('None (Top Level)') + ->searchable(), + Forms\Components\Select::make('permission') + ->label('Required Permission') + ->options(fn () => Permission::pluck('name', 'name')) + ->searchable() + ->placeholder('No specific permission required') + ->helperText('If set, user must have this permission to see this item'), + Forms\Components\TextInput::make('icon') + ->placeholder('heroicon-o-home') + ->helperText('Heroicon name or custom icon class'), + Forms\Components\Select::make('target') + ->options([ + '_self' => 'Same Window', + '_blank' => 'New Tab', + ]) + ->default('_self'), + Forms\Components\TextInput::make('order') + ->numeric() + ->default(0) + ->helperText('Lower numbers appear first'), + Forms\Components\Toggle::make('is_active') + ->label('Active') + ->default(true), + ])->columns(2), + ]); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('title') + ->reorderable('order') + ->defaultSort('order') + ->columns([ + Tables\Columns\TextColumn::make('title') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('parent.title') + ->label('Parent') + ->placeholder('—') + ->badge() + ->color('gray'), + Tables\Columns\TextColumn::make('type') + ->badge() + ->color(fn (string $state) => match ($state) { + 'link' => 'info', + 'route' => 'success', + 'module' => 'warning', + default => 'gray', + }), + Tables\Columns\TextColumn::make('module') + ->placeholder('—') + ->toggleable(), + Tables\Columns\TextColumn::make('permission') + ->placeholder('—') + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('order') + ->sortable(), + Tables\Columns\IconColumn::make('is_active') + ->label('Active') + ->boolean(), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('type') + ->options([ + 'link' => 'External Link', + 'route' => 'Named Route', + 'module' => 'Module', + ]), + Tables\Filters\TernaryFilter::make('is_active') + ->label('Active'), + ]) + ->headerActions([ + Tables\Actions\CreateAction::make(), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/src/app/Models/Menu.php b/src/app/Models/Menu.php new file mode 100644 index 0000000..454c162 --- /dev/null +++ b/src/app/Models/Menu.php @@ -0,0 +1,40 @@ + 'boolean', + ]; + + public function items(): HasMany + { + return $this->hasMany(MenuItem::class)->whereNull('parent_id')->orderBy('order'); + } + + public function allItems(): HasMany + { + return $this->hasMany(MenuItem::class)->orderBy('order'); + } + + public static function findBySlug(string $slug): ?self + { + return static::where('slug', $slug)->where('is_active', true)->first(); + } + + public static function findByLocation(string $location): ?self + { + return static::where('location', $location)->where('is_active', true)->first(); + } +} diff --git a/src/app/Models/MenuItem.php b/src/app/Models/MenuItem.php new file mode 100644 index 0000000..61b2181 --- /dev/null +++ b/src/app/Models/MenuItem.php @@ -0,0 +1,90 @@ + 'array', + 'is_active' => 'boolean', + 'order' => 'integer', + ]; + + public function menu(): BelongsTo + { + return $this->belongsTo(Menu::class); + } + + public function parent(): BelongsTo + { + return $this->belongsTo(MenuItem::class, 'parent_id'); + } + + public function children(): HasMany + { + return $this->hasMany(MenuItem::class, 'parent_id')->orderBy('order'); + } + + public function getUrlAttribute(): ?string + { + return match ($this->type) { + 'link' => $this->attributes['url'], + 'route' => $this->route ? route($this->route, $this->route_params ?? []) : null, + 'module' => $this->module ? route("{$this->module}.index") : null, + default => null, + }; + } + + public function isVisibleToUser(?User $user = null): bool + { + if (!$this->is_active) { + return false; + } + + $user = $user ?? Auth::user(); + + if (!$user) { + return !$this->permission && !$this->module; + } + + if ($user->hasRole('admin')) { + return true; + } + + if ($this->permission) { + return $user->can($this->permission); + } + + if ($this->module) { + return $user->can("{$this->module}.view"); + } + + return true; + } + + public function getVisibleChildren(?User $user = null): \Illuminate\Support\Collection + { + return $this->children->filter(fn (MenuItem $item) => $item->isVisibleToUser($user)); + } +} diff --git a/src/app/Services/MenuService.php b/src/app/Services/MenuService.php new file mode 100644 index 0000000..e355c08 --- /dev/null +++ b/src/app/Services/MenuService.php @@ -0,0 +1,117 @@ +buildMenuTree($menu, $user); + } + + public function buildMenuTree(Menu $menu, ?User $user = null): array + { + $user = $user ?? Auth::user(); + + $items = $menu->items() + ->with(['children' => fn ($q) => $q->orderBy('order')]) + ->where('is_active', true) + ->get(); + + return $this->filterAndMapItems($items, $user); + } + + protected function filterAndMapItems(Collection $items, ?User $user): array + { + return $items + ->filter(fn (MenuItem $item) => $item->isVisibleToUser($user)) + ->map(fn (MenuItem $item) => $this->mapItem($item, $user)) + ->values() + ->toArray(); + } + + protected function mapItem(MenuItem $item, ?User $user): array + { + $children = $item->children->count() > 0 + ? $this->filterAndMapItems($item->children, $user) + : []; + + return [ + 'id' => $item->id, + 'title' => $item->title, + 'url' => $item->url, + 'icon' => $item->icon, + 'target' => $item->target, + 'is_active' => $this->isActiveUrl($item->url), + 'children' => $children, + 'has_children' => count($children) > 0, + ]; + } + + protected function isActiveUrl(?string $url): bool + { + if (!$url) { + return false; + } + + $currentUrl = request()->url(); + $currentPath = request()->path(); + + if ($url === $currentUrl) { + return true; + } + + $urlPath = parse_url($url, PHP_URL_PATH) ?? ''; + + return $urlPath && str_starts_with('/' . ltrim($currentPath, '/'), $urlPath); + } + + public function getAvailableModules(): array + { + $modules = []; + $modulesPath = app_path('Modules'); + + if (!is_dir($modulesPath)) { + return $modules; + } + + foreach (scandir($modulesPath) as $module) { + if ($module === '.' || $module === '..' || !is_dir($modulesPath . '/' . $module)) { + continue; + } + + $slug = strtolower(preg_replace('/(?getRoutes() as $route) { + $name = $route->getName(); + if ($name && !str_starts_with($name, 'filament.') && !str_starts_with($name, 'livewire.')) { + $routes[$name] = $name; + } + } + + ksort($routes); + return $routes; + } +} diff --git a/src/app/View/Components/FrontendMenu.php b/src/app/View/Components/FrontendMenu.php new file mode 100644 index 0000000..985148c --- /dev/null +++ b/src/app/View/Components/FrontendMenu.php @@ -0,0 +1,30 @@ +items = $menuService->getMenu($menu) ?? []; + } + + public function render(): View + { + return view('components.frontend-menu'); + } + + public function hasItems(): bool + { + return count($this->items) > 0; + } +} diff --git a/src/app/View/Components/FrontendMenuResponsive.php b/src/app/View/Components/FrontendMenuResponsive.php new file mode 100644 index 0000000..0b3d5e9 --- /dev/null +++ b/src/app/View/Components/FrontendMenuResponsive.php @@ -0,0 +1,30 @@ +items = $menuService->getMenu($menu) ?? []; + } + + public function render(): View + { + return view('components.frontend-menu-responsive'); + } + + public function hasItems(): bool + { + return count($this->items) > 0; + } +} diff --git a/src/database/migrations/2024_01_01_000001_create_menus_table.php b/src/database/migrations/2024_01_01_000001_create_menus_table.php new file mode 100644 index 0000000..efa1381 --- /dev/null +++ b/src/database/migrations/2024_01_01_000001_create_menus_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->string('location')->nullable()->comment('e.g., header, footer, sidebar'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('menus'); + } +}; diff --git a/src/database/migrations/2024_01_01_000002_create_menu_items_table.php b/src/database/migrations/2024_01_01_000002_create_menu_items_table.php new file mode 100644 index 0000000..5da8233 --- /dev/null +++ b/src/database/migrations/2024_01_01_000002_create_menu_items_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('menu_id')->constrained()->cascadeOnDelete(); + $table->foreignId('parent_id')->nullable()->constrained('menu_items')->cascadeOnDelete(); + $table->string('title'); + $table->string('type')->default('link')->comment('link, module, route'); + $table->string('url')->nullable()->comment('For external links'); + $table->string('route')->nullable()->comment('For named routes'); + $table->json('route_params')->nullable()->comment('Route parameters as JSON'); + $table->string('module')->nullable()->comment('Module slug for permission checking'); + $table->string('permission')->nullable()->comment('Specific permission required'); + $table->string('icon')->nullable()->comment('Icon class or name'); + $table->string('target')->default('_self')->comment('_self, _blank'); + $table->integer('order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('menu_items'); + } +}; diff --git a/src/database/seeders/DatabaseSeeder.php b/src/database/seeders/DatabaseSeeder.php index dea5ddb..1b44b30 100644 --- a/src/database/seeders/DatabaseSeeder.php +++ b/src/database/seeders/DatabaseSeeder.php @@ -13,6 +13,7 @@ public function run(): void { $this->call([ RolePermissionSeeder::class, + MenuSeeder::class, ]); } } diff --git a/src/database/seeders/MenuSeeder.php b/src/database/seeders/MenuSeeder.php new file mode 100644 index 0000000..6e13df3 --- /dev/null +++ b/src/database/seeders/MenuSeeder.php @@ -0,0 +1,33 @@ + 'header'], + [ + 'name' => 'Header Navigation', + 'location' => 'header', + 'is_active' => true, + ] + ); + + MenuItem::firstOrCreate( + ['menu_id' => $headerMenu->id, 'title' => 'Dashboard'], + [ + 'type' => 'route', + 'route' => 'dashboard', + 'icon' => 'heroicon-o-home', + 'order' => 1, + 'is_active' => true, + ] + ); + } +} diff --git a/src/database/seeders/RolePermissionSeeder.php b/src/database/seeders/RolePermissionSeeder.php index bb0fbaf..5a2b30a 100644 --- a/src/database/seeders/RolePermissionSeeder.php +++ b/src/database/seeders/RolePermissionSeeder.php @@ -22,6 +22,10 @@ public function run(): void 'users.delete', 'settings.manage', 'audit.view', + 'menus.view', + 'menus.create', + 'menus.edit', + 'menus.delete', ]; foreach ($corePermissions as $permission) { diff --git a/src/resources/views/components/frontend-menu-responsive.blade.php b/src/resources/views/components/frontend-menu-responsive.blade.php new file mode 100644 index 0000000..a19e69d --- /dev/null +++ b/src/resources/views/components/frontend-menu-responsive.blade.php @@ -0,0 +1,47 @@ +@if($hasItems()) +
merge(['class' => $class]) }}> + @foreach($items as $item) + @if($item['has_children']) + {{-- Collapsible menu item with children --}} +
+ + +
+ @foreach($item['children'] as $child) + + @if($child['icon']) + + @endif + {{ $child['title'] }} + + @endforeach +
+
+ @else + {{-- Regular menu item --}} + + @if($item['icon']) + + @endif + {{ $item['title'] }} + + @endif + @endforeach +
+@endif diff --git a/src/resources/views/components/frontend-menu.blade.php b/src/resources/views/components/frontend-menu.blade.php new file mode 100644 index 0000000..e4fd3aa --- /dev/null +++ b/src/resources/views/components/frontend-menu.blade.php @@ -0,0 +1,60 @@ +@if($hasItems()) + +@endif diff --git a/src/resources/views/layouts/navigation.blade.php b/src/resources/views/layouts/navigation.blade.php index c64bf64..41f42f6 100644 --- a/src/resources/views/layouts/navigation.blade.php +++ b/src/resources/views/layouts/navigation.blade.php @@ -11,10 +11,8 @@ - @@ -67,9 +65,7 @@