diff --git a/extensions/Others/NavbarManager/Admin/Resources/NavbarLinkResource.php b/extensions/Others/NavbarManager/Admin/Resources/NavbarLinkResource.php new file mode 100644 index 000000000..013bf9dbd --- /dev/null +++ b/extensions/Others/NavbarManager/Admin/Resources/NavbarLinkResource.php @@ -0,0 +1,464 @@ + [], + 'Client Routes' => [], + 'API Routes' => [], + 'Other Routes' => [], + ]; + + foreach ($routeCollection as $route) { + $name = $route->getName(); + + if ($name && !str_starts_with($name, 'admin.') + && !str_starts_with($name, 'filament.') + && !str_starts_with($name, '_debugbar') + && !str_starts_with($name, 'telescope') + && !str_starts_with($name, 'clockwork')) { + + $methods = implode('|', $route->methods()); + $uri = $route->uri(); + $label = "{$name} ({$methods} /{$uri})"; + + // Group routes by type + if (in_array($name, ['home', 'dashboard', 'services', 'invoices', 'profile', 'shop'])) { + $grouped['Common Routes'][$name] = $label; + } elseif (str_starts_with($name, 'client.') || str_starts_with($name, 'account.')) { + $grouped['Client Routes'][$name] = $label; + } elseif (str_starts_with($name, 'api.')) { + $grouped['API Routes'][$name] = $label; + } else { + $grouped['Other Routes'][$name] = $label; + } + } + } + + // Remove empty groups and flatten for Filament + foreach ($grouped as $group => $groupRoutes) { + if (!empty($groupRoutes)) { + asort($groupRoutes); + $routes = array_merge($routes, $groupRoutes); + } + } + + } catch (\Exception $e) { + // Fallback routes if there's an error + $routes = [ + 'home' => 'home (GET /)', + 'dashboard' => 'dashboard (GET dashboard)', + 'services' => 'services (GET services)', + 'invoices' => 'invoices (GET invoices)', + 'profile' => 'profile (GET profile)', + 'shop' => 'shop (GET shop)', + ]; + } + + return $routes; + } + + public static function getAvailableProducts(): array + { + $products = []; + + try { + $productModels = \App\Models\Product::where('hidden', false)->get(); + + foreach ($productModels as $product) { + $label = $product->name; + + // Add pricing info if available + if ($product->price > 0) { + $label .= " (${$product->price})"; + } + + // Add description if available + if ($product->description) { + $description = \Str::limit($product->description, 50); + $label .= " - {$description}"; + } + + $products[$product->id] = $label; + } + + } catch (\Exception $e) { + // Fallback if there's an error + $products = [ + 1 => 'Minecraft Server - 1GB RAM', + 2 => 'Minecraft Server - 6GB RAM', + 3 => 'Rust Server - 4GB RAM', + ]; + } + + return $products; + } + + public static function getAvailableCategories(): array + { + $categories = []; + + try { + $categoryModels = \App\Models\Category::all(); + + foreach ($categoryModels as $category) { + $label = $category->name; + + // Add parent category if it's a subcategory + if ($category->parent) { + $label = $category->parent->name . ' > ' . $label; + } + + // Add product count if available + $productCount = $category->products()->where('hidden', false)->count(); + if ($productCount > 0) { + $label .= " ({$productCount} products)"; + } + + $categories[$category->id] = $label; + } + + } catch (\Exception $e) { + // Fallback if there's an error + $categories = [ + 1 => 'Gaming Servers', + 2 => 'Web Hosting', + 3 => 'VPS Hosting', + ]; + } + + return $categories; + } + + public static function form(Schema $schema): Schema + { + return $schema + ->schema([ + Forms\Components\TextInput::make('name') + ->label('Link Name') + ->required() + ->maxLength(255), + + Forms\Components\Select::make('type') + ->label('Navigation Type') + ->options([ + 'primary' => 'Primary Navigation', + 'account' => 'Account Navigation', + 'dashboard' => 'Dashboard Navigation', + ]) + ->required() + ->default('primary'), + + Forms\Components\Select::make('parent_id') + ->label('Parent Link (for dropdown)') + ->relationship('parent', 'name') + ->searchable() + ->preload() + ->placeholder('Select parent (optional)'), + + Section::make('Link Destination') + ->description('Configure the link destination') + ->schema([ + Forms\Components\Select::make('link_type') + ->label('Link Type') + ->required() + ->live() + ->options([ + 'route' => 'Laravel Route', + 'path' => 'Custom Path', + 'url' => 'External URL', + 'product' => 'Product Path', + 'category' => 'Category Path', + ]) + ->default('route') + ->helperText('Select the type of link you want to create'), + + Forms\Components\Select::make('route_name') + ->label('Route Name') + ->required(fn (Get $get): bool => $get('link_type') === 'route') + ->options(self::getAvailableRoutes()) + ->searchable() + ->placeholder('Select a Laravel route') + ->helperText('Choose from available Laravel routes') + ->visible(fn (Get $get): bool => $get('link_type') === 'route'), + + Forms\Components\Repeater::make('route_params') + ->label('Route Parameters') + ->schema([ + Forms\Components\TextInput::make('key') + ->label('Parameter') + ->required(), + Forms\Components\TextInput::make('value') + ->label('Value') + ->required(), + ]) + ->columns(2) + ->helperText('Parameters to pass to the route (optional)') + ->visible(fn (Get $get): bool => $get('link_type') === 'route'), + + Forms\Components\TextInput::make('custom_path') + ->label('Custom Path') + ->required(fn (Get $get): bool => $get('link_type') === 'path') + ->placeholder('e.g., /admin/custom-page') + ->helperText('Internal path on your website') + ->visible(fn (Get $get): bool => $get('link_type') === 'path'), + + Forms\Components\TextInput::make('custom_url') + ->label('External URL') + ->required(fn (Get $get): bool => $get('link_type') === 'url') + ->url() + ->placeholder('e.g., https://example.com') + ->helperText('Full external URL') + ->visible(fn (Get $get): bool => $get('link_type') === 'url'), + + Forms\Components\Select::make('product_id') + ->label('Select Product') + ->required(fn (Get $get): bool => $get('link_type') === 'product') + ->options(self::getAvailableProducts()) + ->searchable() + ->placeholder('Choose a product') + ->helperText('Select which product this link should point to') + ->visible(fn (Get $get): bool => $get('link_type') === 'product'), + + Forms\Components\Select::make('category_id') + ->label('Select Category') + ->required(fn (Get $get): bool => $get('link_type') === 'category') + ->options(self::getAvailableCategories()) + ->searchable() + ->placeholder('Choose a category') + ->helperText('Select which category this link should point to') + ->visible(fn (Get $get): bool => $get('link_type') === 'category'), + ]) + ->columns(2), + + Forms\Components\Select::make('icon') + ->label('Icon') + ->searchable() + ->options([ + 'ri-home-line' => 'Home', + 'ri-dashboard-line' => 'Dashboard', + 'ri-user-line' => 'User', + 'ri-settings-line' => 'Settings', + 'ri-file-line' => 'File', + 'ri-folder-line' => 'Folder', + 'ri-mail-line' => 'Mail', + 'ri-phone-line' => 'Phone', + 'ri-calendar-line' => 'Calendar', + 'ri-time-line' => 'Time', + 'ri-notification-line' => 'Notification', + 'ri-search-line' => 'Search', + 'ri-add-line' => 'Add', + 'ri-edit-line' => 'Edit', + 'ri-delete-line' => 'Delete', + 'ri-arrow-right-line' => 'Arrow Right', + 'ri-arrow-left-line' => 'Arrow Left', + 'ri-arrow-up-line' => 'Arrow Up', + 'ri-arrow-down-line' => 'Arrow Down', + 'ri-link-line' => 'Link', + 'ri-external-link-line' => 'External Link', + 'ri-bookmark-line' => 'Bookmark', + 'ri-star-line' => 'Star', + 'ri-heart-line' => 'Heart', + 'ri-eye-line' => 'Eye', + 'ri-lock-line' => 'Lock', + 'ri-unlock-line' => 'Unlock', + 'ri-shield-line' => 'Shield', + 'ri-check-line' => 'Check', + 'ri-close-line' => 'Close', + 'ri-information-line' => 'Information', + 'ri-question-line' => 'Question', + 'ri-warning-line' => 'Warning', + 'ri-error-warning-line' => 'Error', + ]) + ->placeholder('Select an icon') + ->helperText('RemixIcon class name'), + + Forms\Components\Select::make('condition') + ->label('Visibility Condition') + ->options([ + 'always_visible' => 'Always Show', + 'admin_only' => 'Admin Only', + 'clients_only' => 'Client Only', + 'guests_only' => 'Guest Only', + 'always_hidden' => 'Always Hidden', + ]) + ->required() + ->default('always_visible'), + + Forms\Components\Toggle::make('is_active') + ->label('Active') + ->default(true), + + Forms\Components\TextInput::make('sort_order') + ->label('Sort Order') + ->numeric() + ->default(0) + ->helperText('Lower numbers appear first'), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->label('Name') + ->searchable() + ->sortable(), + + Tables\Columns\TextColumn::make('type') + ->label('Type') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'primary' => 'primary', + 'account' => 'success', + 'dashboard' => 'warning', + default => 'gray', + }), + + Tables\Columns\TextColumn::make('link_type') + ->label('Link Type') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'route' => 'primary', + 'path' => 'warning', + 'url' => 'danger', + 'product' => 'success', + 'category' => 'info', + default => 'gray', + }) + ->formatStateUsing(fn (string $state): string => match ($state) { + 'route' => 'Laravel Route', + 'path' => 'Custom Path', + 'url' => 'External URL', + 'product' => 'Product Path', + 'category' => 'Category Path', + default => $state, + }), + + Tables\Columns\TextColumn::make('parent.name') + ->label('Parent') + ->placeholder('—'), + + Tables\Columns\TextColumn::make('route_name') + ->label('Route') + ->placeholder('—') + ->limit(30), + + Tables\Columns\TextColumn::make('custom_path') + ->label('Path') + ->placeholder('—') + ->limit(30), + + Tables\Columns\TextColumn::make('custom_url') + ->label('URL') + ->placeholder('—') + ->limit(30), + + Tables\Columns\TextColumn::make('product.name') + ->label('Product') + ->placeholder('—') + ->limit(25), + + Tables\Columns\TextColumn::make('category.name') + ->label('Category') + ->placeholder('—') + ->limit(25), + + Tables\Columns\IconColumn::make('is_active') + ->label('Active') + ->boolean(), + + Tables\Columns\TextColumn::make('condition') + ->label('Condition') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'always_visible' => 'success', + 'admin_only' => 'danger', + 'clients_only' => 'primary', + 'guests_only' => 'warning', + 'always_hidden' => 'gray', + default => 'gray', + }), + + Tables\Columns\TextColumn::make('sort_order') + ->label('Order') + ->sortable(), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('type') + ->options([ + 'primary' => 'Primary Navigation', + 'account' => 'Account Navigation', + 'dashboard' => 'Dashboard Navigation', + ]), + + Tables\Filters\SelectFilter::make('condition') + ->options([ + 'always_visible' => 'Always Show', + 'admin_only' => 'Admin Only', + 'clients_only' => 'Client Only', + 'guests_only' => 'Guest Only', + 'always_hidden' => 'Always Hidden', + ]), + + Tables\Filters\TernaryFilter::make('is_active') + ->label('Active Status'), + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]) + ->defaultSort('sort_order'); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListNavbarLinks::route('/'), + 'create' => Pages\CreateNavbarLink::route('/create'), + 'edit' => Pages\EditNavbarLink::route('/{record}/edit'), + ]; + } +} diff --git a/extensions/Others/NavbarManager/Admin/Resources/NavbarLinkResource/Pages/CreateNavbarLink.php b/extensions/Others/NavbarManager/Admin/Resources/NavbarLinkResource/Pages/CreateNavbarLink.php new file mode 100644 index 000000000..4b0e23e6a --- /dev/null +++ b/extensions/Others/NavbarManager/Admin/Resources/NavbarLinkResource/Pages/CreateNavbarLink.php @@ -0,0 +1,11 @@ + 'Help Center', + 'type' => 'primary', + 'link_type' => 'url', + 'custom_url' => 'https://docs.example.com', + 'icon' => 'ri-question-line', + 'condition' => 'always_visible', + 'sort_order' => 100, + 'is_active' => true, + ]); + + NavbarLink::create([ + 'name' => 'Status Page', + 'type' => 'primary', + 'link_type' => 'url', + 'custom_url' => 'https://status.example.com', + 'icon' => 'ri-pulse-line', + 'condition' => 'always_visible', + 'sort_order' => 110, + 'is_active' => true, + ]); + + // Sample Account Dropdown Link + NavbarLink::create([ + 'name' => 'Billing History', + 'type' => 'account', + 'link_type' => 'route', + 'route_name' => 'invoices', + 'icon' => 'ri-history-line', + 'condition' => 'clients_only', + 'sort_order' => 100, + 'is_active' => true, + ]); + + // Sample Dashboard Link + NavbarLink::create([ + 'name' => 'Knowledge Base', + 'type' => 'dashboard', + 'link_type' => 'url', + 'custom_url' => 'https://kb.example.com', + 'icon' => 'ri-book-line', + 'condition' => 'clients_only', + 'sort_order' => 100, + 'is_active' => true, + ]); + + // Sample with dropdown (parent-child) + $parent = NavbarLink::create([ + 'name' => 'Resources', + 'type' => 'primary', + 'link_type' => 'route', + 'route_name' => 'home', // fallback + 'icon' => 'ri-folder-line', + 'condition' => 'always_visible', + 'sort_order' => 90, + 'is_active' => true, + ]); + + NavbarLink::create([ + 'name' => 'Downloads', + 'type' => 'primary', + 'link_type' => 'path', + 'custom_path' => '/downloads', + 'parent_id' => $parent->id, + 'condition' => 'always_visible', + 'sort_order' => 10, + 'is_active' => true, + ]); + + NavbarLink::create([ + 'name' => 'Documentation', + 'type' => 'primary', + 'link_type' => 'url', + 'custom_url' => 'https://docs.example.com', + 'parent_id' => $parent->id, + 'condition' => 'always_visible', + 'sort_order' => 20, + 'is_active' => true, + ]); + } +} diff --git a/extensions/Others/NavbarManager/Database/migrations/2024_07_02_120000_create_navbar_links_table.php b/extensions/Others/NavbarManager/Database/migrations/2024_07_02_120000_create_navbar_links_table.php new file mode 100644 index 000000000..1bfa4ba4c --- /dev/null +++ b/extensions/Others/NavbarManager/Database/migrations/2024_07_02_120000_create_navbar_links_table.php @@ -0,0 +1,42 @@ +id(); + $table->string('name'); + $table->enum('type', ['primary', 'account', 'dashboard'])->default('primary'); + $table->enum('link_type', ['route', 'path', 'url'])->default('route'); + $table->string('route_name')->nullable(); + $table->json('route_params')->nullable(); + $table->string('custom_path')->nullable(); + $table->string('custom_url')->nullable(); + $table->string('icon')->nullable(); + $table->enum('condition', [ + 'always_visible', + 'always_hidden', + 'clients_only', + 'admin_only', + 'guests_only' + ])->default('always_visible'); + $table->integer('sort_order')->default(0); + $table->boolean('is_active')->default(true); + $table->unsignedBigInteger('parent_id')->nullable(); + $table->timestamps(); + + $table->foreign('parent_id')->references('id')->on('navbar_links')->onDelete('cascade'); + $table->index(['type', 'is_active', 'sort_order']); + }); + } + + public function down() + { + Schema::dropIfExists('navbar_links'); + } +}; diff --git a/extensions/Others/NavbarManager/Database/migrations/2024_07_02_210000_add_default_override_to_navbar_links_table.php b/extensions/Others/NavbarManager/Database/migrations/2024_07_02_210000_add_default_override_to_navbar_links_table.php new file mode 100644 index 000000000..6e1b15930 --- /dev/null +++ b/extensions/Others/NavbarManager/Database/migrations/2024_07_02_210000_add_default_override_to_navbar_links_table.php @@ -0,0 +1,24 @@ +boolean('is_default_override')->default(false)->after('link_type'); + $table->string('default_route_key')->nullable()->after('is_default_override'); + $table->integer('default_priority')->nullable()->after('default_route_key'); + }); + } + + public function down() + { + Schema::table('navbar_links', function (Blueprint $table) { + $table->dropColumn(['is_default_override', 'default_route_key', 'default_priority']); + }); + } +}; diff --git a/extensions/Others/NavbarManager/Database/migrations/2024_07_02_220000_add_product_category_to_navbar_links_table.php b/extensions/Others/NavbarManager/Database/migrations/2024_07_02_220000_add_product_category_to_navbar_links_table.php new file mode 100644 index 000000000..c0b66d637 --- /dev/null +++ b/extensions/Others/NavbarManager/Database/migrations/2024_07_02_220000_add_product_category_to_navbar_links_table.php @@ -0,0 +1,27 @@ +unsignedBigInteger('product_id')->nullable()->after('custom_url'); + $table->unsignedBigInteger('category_id')->nullable()->after('product_id'); + + // Add foreign key constraints if needed + // $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); + // $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade'); + }); + } + + public function down() + { + Schema::table('navbar_links', function (Blueprint $table) { + $table->dropColumn(['product_id', 'category_id']); + }); + } +}; diff --git a/extensions/Others/NavbarManager/Database/migrations/2024_07_02_230000_update_navbar_links_link_type_enum.php b/extensions/Others/NavbarManager/Database/migrations/2024_07_02_230000_update_navbar_links_link_type_enum.php new file mode 100644 index 000000000..16fedded2 --- /dev/null +++ b/extensions/Others/NavbarManager/Database/migrations/2024_07_02_230000_update_navbar_links_link_type_enum.php @@ -0,0 +1,19 @@ + self::extractDefaultRoutes('getLinks'), + 'dashboard' => self::extractDefaultRoutes('getDashboardLinks'), + 'account' => self::extractDefaultRoutes('getAccountDropdownLinks'), + ]; + + return $routes; + } + + /** + * Extract default routes from a Navigation method (safe - no events) + */ + private static function extractDefaultRoutes($method) + { + try { + // Instead of calling Navigation methods that trigger events, + // we'll recreate the core logic without events + switch ($method) { + case 'getLinks': + return self::getCoreLinksWithoutEvents(); + case 'getDashboardLinks': + return self::getCoreDashboardLinksWithoutEvents(); + case 'getAccountDropdownLinks': + return self::getCoreAccountLinksWithoutEvents(); + default: + return []; + } + } catch (\Exception $e) { + \Log::error('DefaultRouteManager: Error extracting routes from ' . $method, [ + 'error' => $e->getMessage() + ]); + return []; + } + } + + /** + * Get core primary navigation links without triggering events + */ + private static function getCoreLinksWithoutEvents() + { + $categories = \App\Models\Category::whereNull('parent_id')->where(function ($query) { + $query->whereHas('children')->orWhereHas('products', function ($query) { + $query->where('hidden', false); + }); + })->get(); + + $routes = [ + [ + 'name' => __('navigation.home'), + 'route' => 'home', + 'icon' => 'ri-home-2', + 'priority' => 10, + ], + [ + 'name' => __('navigation.shop'), + 'route' => 'shop', + 'icon' => 'ri-shopping-bag', + 'priority' => 20, + 'children' => $categories->map(function ($category, $index) { + return [ + 'name' => $category->name, + 'route' => 'category.show', + 'params' => ['category' => $category->slug], + 'priority' => $index * 10, + ]; + })->toArray(), + 'condition' => count($categories) > 0, + ], + ]; + + $extractedRoutes = []; + foreach ($routes as $index => $route) { + if (isset($route['route'])) { + $extractedRoutes[] = [ + 'key' => $route['route'] . '_' . $index, + 'name' => $route['name'] ?? ucfirst($route['route']), + 'route' => $route['route'], + 'icon' => $route['icon'] ?? null, + 'priority' => $route['priority'] ?? $index * 10, + 'params' => $route['params'] ?? null, + 'condition' => isset($route['condition']) ? (is_bool($route['condition']) ? ($route['condition'] ? 'always_visible' : 'always_hidden') : 'custom') : 'always_visible', + 'children' => isset($route['children']) ? self::extractChildRoutes($route['children'], $route['route']) : [], + ]; + } + } + + return $extractedRoutes; + } + + /** + * Get core dashboard links without triggering events + */ + private static function getCoreDashboardLinksWithoutEvents() + { + $routes = [ + [ + 'name' => __('navigation.dashboard'), + 'route' => 'dashboard', + 'icon' => 'ri-function', + 'priority' => 10, + ], + [ + 'name' => __('navigation.services'), + 'route' => 'services', + 'icon' => 'ri-archive-stack', + 'priority' => 20, + ], + [ + 'name' => __('navigation.invoices'), + 'route' => 'invoices', + 'icon' => 'ri-receipt', + 'priority' => 30, + ], + ]; + + $extractedRoutes = []; + foreach ($routes as $index => $route) { + if (isset($route['route'])) { + $extractedRoutes[] = [ + 'key' => $route['route'] . '_' . $index, + 'name' => $route['name'] ?? ucfirst($route['route']), + 'route' => $route['route'], + 'icon' => $route['icon'] ?? null, + 'priority' => $route['priority'] ?? $index * 10, + 'params' => $route['params'] ?? null, + 'condition' => 'always_visible', + 'children' => [], + ]; + } + } + + return $extractedRoutes; + } + + /** + * Get core account links without triggering events + */ + private static function getCoreAccountLinksWithoutEvents() + { + $routes = [ + [ + 'name' => __('navigation.profile'), + 'route' => 'profile', + 'icon' => 'ri-user', + 'priority' => 10, + ], + [ + 'name' => __('navigation.logout'), + 'route' => 'auth.logout', + 'icon' => 'ri-logout', + 'priority' => 90, + ], + ]; + + $extractedRoutes = []; + foreach ($routes as $index => $route) { + if (isset($route['route'])) { + $extractedRoutes[] = [ + 'key' => $route['route'] . '_' . $index, + 'name' => $route['name'] ?? ucfirst($route['route']), + 'route' => $route['route'], + 'icon' => $route['icon'] ?? null, + 'priority' => $route['priority'] ?? $index * 10, + 'params' => $route['params'] ?? null, + 'condition' => 'always_visible', + 'children' => [], + ]; + } + } + + return $extractedRoutes; + } + + /** + * Extract child routes from parent route + */ + private static function extractChildRoutes($children, $parentRoute) + { + $extractedChildren = []; + + foreach ($children as $index => $child) { + if (isset($child['route'])) { + $extractedChildren[] = [ + 'key' => $parentRoute . '_child_' . $child['route'] . '_' . $index, + 'name' => $child['name'] ?? ucfirst($child['route']), + 'route' => $child['route'], + 'icon' => $child['icon'] ?? null, + 'priority' => $child['priority'] ?? $index * 10, + 'params' => $child['params'] ?? null, + ]; + } + } + + return $extractedChildren; + } + + /** + * Sync default routes to database for editing + */ + public static function syncDefaultRoutes($type = 'primary') + { + $defaultRoutes = self::getDefaultRoutes()[$type] ?? []; + $synced = 0; + + foreach ($defaultRoutes as $route) { + $existing = NavbarLink::where('default_route_key', $route['key']) + ->where('type', $type) + ->first(); + + if (!$existing) { + NavbarLink::create([ + 'name' => $route['name'], + 'type' => $type, + 'link_type' => 'route', + 'route_name' => $route['route'], + 'route_params' => $route['params'], + 'icon' => $route['icon'], + 'condition' => $route['condition'], + 'sort_order' => $route['priority'], + 'is_active' => true, + 'is_default_override' => true, + 'default_route_key' => $route['key'], + 'default_priority' => $route['priority'], + ]); + $synced++; + } + + // Sync children + foreach ($route['children'] as $child) { + $existingChild = NavbarLink::where('default_route_key', $child['key']) + ->where('type', $type) + ->first(); + + if (!$existingChild && $existing) { + NavbarLink::create([ + 'name' => $child['name'], + 'type' => $type, + 'link_type' => 'route', + 'route_name' => $child['route'], + 'route_params' => $child['params'], + 'icon' => $child['icon'], + 'sort_order' => $child['priority'], + 'is_active' => true, + 'parent_id' => $existing->id, + 'is_default_override' => true, + 'default_route_key' => $child['key'], + 'default_priority' => $child['priority'], + ]); + $synced++; + } + } + } + + return $synced; + } + + /** + * Get merged navigation (default + custom) + */ + public static function getMergedNavigation($type = 'primary') + { + // Get default routes that are NOT overridden + $defaultRoutes = self::getDefaultRoutes()[$type] ?? []; + $overriddenKeys = NavbarLink::defaultOverrides() + ->where('type', $type) + ->pluck('default_route_key') + ->toArray(); + + $mergedRoutes = []; + + // Add non-overridden default routes + foreach ($defaultRoutes as $route) { + if (!in_array($route['key'], $overriddenKeys)) { + $mergedRoutes[] = [ + 'name' => $route['name'], + 'route' => $route['route'], + 'icon' => $route['icon'], + 'priority' => $route['priority'], + 'params' => $route['params'], + 'spa' => true, + 'source' => 'default', + ]; + } + } + + // Add custom and overridden routes from database + $customRoutes = NavbarLink::active() + ->topLevel() + ->where('type', $type) + ->orderBy('sort_order') + ->get(); + + foreach ($customRoutes as $link) { + $formatted = self::formatNavbarLink($link); + if ($formatted) { + $formatted['source'] = $link->is_default_override ? 'override' : 'custom'; + $mergedRoutes[] = $formatted; + } + } + + // Sort by priority + usort($mergedRoutes, function ($a, $b) { + return ($a['priority'] ?? 0) <=> ($b['priority'] ?? 0); + }); + + return $mergedRoutes; + } + + /** + * Format NavbarLink model to navigation array format + */ + private static function formatNavbarLink($link) + { + $formatted = [ + 'name' => $link->name, + 'priority' => $link->sort_order, + ]; + + // Add icon if valid + if ($link->icon) { + $baseIcon = preg_replace('/-(?:line|fill)$/', '', $link->icon); + $formatted['icon'] = $baseIcon; + } + + // Handle different link types + switch ($link->link_type) { + case 'route': + if ($link->route_name) { + $formatted['route'] = $link->route_name; + $formatted['spa'] = true; + if ($link->route_params) { + $params = is_string($link->route_params) + ? json_decode($link->route_params, true) + : $link->route_params; + if ($params && is_array($params)) { + $formatted['params'] = $params; + } + } + } else { + return null; + } + break; + case 'path': + case 'url': + $formatted['route'] = 'navbar.redirect'; + $formatted['params'] = ['linkId' => $link->id]; + $formatted['spa'] = false; + break; + } + + // Handle children + if ($link->children && $link->children->count() > 0) { + $children = []; + foreach ($link->children->where('is_active', true) as $child) { + $childFormatted = self::formatNavbarLink($child); + if ($childFormatted) { + $children[] = $childFormatted; + } + } + if (!empty($children)) { + $formatted['children'] = $children; + } + } + + return $formatted; + } +} diff --git a/extensions/Others/NavbarManager/Helpers/NavbarHelper.php b/extensions/Others/NavbarManager/Helpers/NavbarHelper.php new file mode 100644 index 000000000..ab36ea8ba --- /dev/null +++ b/extensions/Others/NavbarManager/Helpers/NavbarHelper.php @@ -0,0 +1,28 @@ +links; + } + + /** + * Render navbar links component + */ + public static function render($type = 'primary', $class = '') + { + return view('navbar-manager::components.navigation-links', [ + 'links' => self::getLinks($type), + 'class' => $class + ]); + } +} diff --git a/extensions/Others/NavbarManager/Models/NavbarLink.php b/extensions/Others/NavbarManager/Models/NavbarLink.php new file mode 100644 index 000000000..6439d85a8 --- /dev/null +++ b/extensions/Others/NavbarManager/Models/NavbarLink.php @@ -0,0 +1,110 @@ + 'boolean', + 'route_params' => 'json', + ]; + + public function parent() + { + return $this->belongsTo(NavbarLink::class, 'parent_id'); + } + + public function children() + { + return $this->hasMany(NavbarLink::class, 'parent_id')->orderBy('sort_order'); + } + + public function product() + { + return $this->belongsTo(\App\Models\Product::class); + } + + public function category() + { + return $this->belongsTo(\App\Models\Category::class); + } + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + public function scopeTopLevel($query) + { + return $query->whereNull('parent_id'); + } + + public function scopeByType($query, $type) + { + return $query->where('type', $type); + } + + protected static function booted() + { + static::saving(function ($link) { + // Clear unused fields based on link_type + switch ($link->link_type) { + case 'route': + $link->custom_path = null; + $link->custom_url = null; + $link->product_id = null; + $link->category_id = null; + break; + case 'path': + $link->route_name = null; + $link->route_params = null; + $link->custom_url = null; + $link->product_id = null; + $link->category_id = null; + break; + case 'url': + $link->route_name = null; + $link->route_params = null; + $link->custom_path = null; + $link->product_id = null; + $link->category_id = null; + break; + case 'product': + $link->route_name = null; + $link->route_params = null; + $link->custom_path = null; + $link->custom_url = null; + $link->category_id = null; + break; + case 'category': + $link->route_name = null; + $link->route_params = null; + $link->custom_path = null; + $link->custom_url = null; + $link->product_id = null; + break; + } + }); + } +} diff --git a/extensions/Others/NavbarManager/NavbarManager.php b/extensions/Others/NavbarManager/NavbarManager.php new file mode 100644 index 000000000..1df2ae663 --- /dev/null +++ b/extensions/Others/NavbarManager/NavbarManager.php @@ -0,0 +1,278 @@ + 'Notice', + 'type' => 'placeholder', + 'label' => new HtmlString('You can use this extension to manage custom navigation links. To manage navbar links, go to Navbar Manager.'), + ], + ]; + } catch (\Exception $e) { + return [ + [ + 'name' => 'Notice', + 'type' => 'placeholder', + 'label' => new HtmlString('You can use this extension to manage custom navigation links. You\'ll need to enable this extension above to get started.'), + ], + ]; + } + } + + public function enabled() + { + // This method is called only when the extension is being enabled + // Run migrations or initial setup here + try { + Artisan::call('migrate', [ + '--path' => 'extensions/Others/NavbarManager/database/migrations', + '--force' => true + ]); + } catch (\Exception $e) { + \Log::error('NavbarManager: Migration failed', ['error' => $e->getMessage()]); + } + } + + public function boot() + { + // Register view service provider + app()->register(\Paymenter\Extensions\Others\NavbarManager\Providers\ViewServiceProvider::class); + + // Register custom routes for navbar links + require __DIR__ . '/routes/web.php'; + + // Register event listeners for navigation - dodaće naše linkove uz postojeće + Event::listen('navigation', function () { + return $this->getNavigationLinks('primary'); + }); + + Event::listen('navigation.account-dropdown', function () { + return $this->getNavigationLinks('account'); + }); + + Event::listen('navigation.dashboard', function () { + return $this->getNavigationLinks('dashboard'); + }); + + // Register the Filament resource + try { + \Filament\Facades\Filament::serving(function () { + \Filament\Facades\Filament::getCurrentPanel() + ->resources([ + NavbarLinkResource::class, + ]); + }); + } catch (\Exception $e) { + // Resource not yet available + } + } + + private function getNavigationLinks($type) + { + try { + $links = NavbarLink::where('type', $type) + ->where('is_active', true) + ->whereNull('parent_id') // Only get top-level links + ->orderBy('sort_order') + ->with('children') + ->get(); + + $navLinks = []; + foreach ($links as $link) { + if ($this->shouldShowLink($link)) { + $formattedLink = $this->formatLink($link); + if ($formattedLink) { + $navLinks[] = $formattedLink; + } + } + } + + return $navLinks; + } catch (\Exception $e) { + // Log error but don't break navigation + \Log::error('NavbarManager: Error loading navigation links', [ + 'type' => $type, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + return []; + } + } + + private function shouldShowLink($link) + { + try { + switch ($link->condition) { + case 'always_visible': + return true; + case 'always_hidden': + return false; + case 'clients_only': + return auth()->check() && auth()->user()->role_id === null; + case 'admin_only': + return auth()->check() && auth()->user()->role_id !== null; + case 'guests_only': + return !auth()->check(); + default: + return true; + } + } catch (\Exception $e) { + // If there's an error checking conditions, default to visible + return true; + } + } + + private function formatLink($link) + { + try { + $formatted = [ + 'name' => $link->name, + 'priority' => $link->sort_order, + ]; + + // Add icon with validation and strip -line/-fill suffix + if ($link->icon && $this->isValidIcon($link->icon)) { + // Remove -line or -fill suffix since Navigation::markActiveRoute will add it + $baseIcon = preg_replace('/-(?:line|fill)$/', '', $link->icon); + $formatted['icon'] = $baseIcon; + } + + // Handle different link types + switch ($link->link_type) { + case 'route': + if ($link->route_name) { + $formatted['route'] = $link->route_name; + $formatted['spa'] = true; // Enable SPA for route links + if ($link->route_params) { + $params = is_string($link->route_params) + ? json_decode($link->route_params, true) + : $link->route_params; + if ($params && is_array($params)) { + $formatted['params'] = $params; + } + } + } else { + return null; + } + break; + case 'path': + case 'url': + // Use navbar redirect route + $formatted['route'] = 'navbar.redirect'; + $formatted['params'] = ['linkId' => $link->id]; + $formatted['spa'] = false; // Disable SPA for external links + break; + case 'product': + if ($link->product_id) { + try { + $product = \App\Models\Product::with('category')->find($link->product_id); + if ($product && $product->category) { + // Use products show route (requires both category and product slugs) + $formatted['route'] = 'products.show'; + $formatted['params'] = [ + 'category' => $product->category->slug ?? $product->category->id, + 'product' => $product->slug ?? $product->id + ]; + $formatted['spa'] = true; + } else { + return null; // Product or category not found + } + } catch (\Exception $e) { + return null; + } + } else { + return null; + } + break; + case 'category': + if ($link->category_id) { + try { + $category = \App\Models\Category::find($link->category_id); + if ($category) { + // Use category show route + $formatted['route'] = 'category.show'; + $formatted['params'] = ['category' => $category->slug ?? $category->id]; + $formatted['spa'] = true; + } else { + return null; // Category not found + } + } catch (\Exception $e) { + return null; + } + } else { + return null; + } + break; + } + + // Handle children (dropdown items) + if ($link->children && $link->children->count() > 0) { + $children = []; + foreach ($link->children->where('is_active', true) as $child) { + if ($this->shouldShowLink($child)) { + $childFormatted = $this->formatLink($child); + if ($childFormatted) { + $children[] = $childFormatted; + } + } + } + if (!empty($children)) { + $formatted['children'] = $children; + } + } + + return $formatted; + } catch (\Exception $e) { + \Log::error('NavbarManager: Error formatting link', [ + 'link_id' => $link->id ?? 'unknown', + 'error' => $e->getMessage() + ]); + return null; + } + } + + private function isValidIcon($icon) + { + // Basic validation for RemixIcon format + if (!preg_match('/^ri-[\w-]+$/', $icon)) { + return false; + } + + // List of common valid RemixIcon names to avoid errors (without -line/-fill suffix) + $commonIcons = [ + 'ri-home', 'ri-user', 'ri-settings', 'ri-dashboard', + 'ri-menu', 'ri-link', 'ri-external-link', 'ri-folder', + 'ri-file', 'ri-edit', 'ri-delete-bin', 'ri-add', + 'ri-navigation', 'ri-admin', 'ri-function', 'ri-archive-stack', + 'ri-receipt', 'ri-customer-service', 'ri-settings-3', 'ri-megaphone', + 'ri-shopping-bag', 'ri-check', 'ri-close', 'ri-arrow-down-s' + ]; + + // Remove -line or -fill suffix if present to get base icon name + $baseIcon = preg_replace('/-(?:line|fill)$/', '', $icon); + + // If it's in our common list, it's definitely valid + if (in_array($baseIcon, $commonIcons)) { + return true; + } + + // For other icons, just check the format (basic validation) + // In a production environment, you might want to check if the SVG file actually exists + return true; + } +} diff --git a/extensions/Others/NavbarManager/Providers/ViewServiceProvider.php b/extensions/Others/NavbarManager/Providers/ViewServiceProvider.php new file mode 100644 index 000000000..24b7e0c95 --- /dev/null +++ b/extensions/Others/NavbarManager/Providers/ViewServiceProvider.php @@ -0,0 +1,26 @@ +loadViewsFrom(__DIR__ . '/../resources/views', 'navbar-manager'); + // Load migrations from the extension so they run when the extension is enabled + $this->loadMigrationsFrom(__DIR__ . '/../Database/migrations'); + + // Register Blade components + Blade::componentNamespace('Paymenter\\Extensions\\Others\\NavbarManager\\View\\Components', 'navbar-manager'); + + // Register global Blade directive for easy navbar usage + Blade::directive('navbarManagerLinks', function ($expression) { + return ""; + }); + } +} diff --git a/extensions/Others/NavbarManager/View/Components/CustomLink.php b/extensions/Others/NavbarManager/View/Components/CustomLink.php new file mode 100644 index 000000000..35e6437d0 --- /dev/null +++ b/extensions/Others/NavbarManager/View/Components/CustomLink.php @@ -0,0 +1,20 @@ +link = $link; + } + + public function render() + { + return view('navbar-manager::components.custom-link'); + } +} diff --git a/extensions/Others/NavbarManager/View/Components/NavigationLinks.php b/extensions/Others/NavbarManager/View/Components/NavigationLinks.php new file mode 100644 index 000000000..d9621ab55 --- /dev/null +++ b/extensions/Others/NavbarManager/View/Components/NavigationLinks.php @@ -0,0 +1,139 @@ +type = $type; + $this->links = $this->getNavigationLinks($type); + } + + public function render() + { + return view('navbar-manager::components.navigation-links'); + } + + private function getNavigationLinks($type) + { + try { + $links = NavbarLink::where('type', $type) + ->where('is_active', true) + ->whereNull('parent_id') + ->orderBy('sort_order') + ->with('children') + ->get(); + + $navLinks = []; + foreach ($links as $link) { + if ($this->shouldShowLink($link)) { + $formattedLink = $this->formatLink($link); + if ($formattedLink) { + $navLinks[] = $formattedLink; + } + } + } + + return $navLinks; + } catch (\Exception $e) { + \Log::error('NavbarManager: Error loading navigation links', [ + 'type' => $type, + 'error' => $e->getMessage() + ]); + return []; + } + } + + private function shouldShowLink($link) + { + try { + switch ($link->condition) { + case 'always_visible': + return true; + case 'always_hidden': + return false; + case 'clients_only': + return auth()->check() && auth()->user()->role_id === null; + case 'admin_only': + return auth()->check() && auth()->user()->role_id !== null; + case 'guests_only': + return !auth()->check(); + default: + return true; + } + } catch (\Exception $e) { + return true; + } + } + + private function formatLink($link) + { + try { + $formatted = [ + 'name' => $link->name, + 'priority' => $link->sort_order, + ]; + + // Add icon + if ($link->icon) { + $formatted['icon'] = $link->icon; + } + + // Handle different link types + switch ($link->link_type) { + case 'route': + if ($link->route_name) { + $formatted['route'] = $link->route_name; + if ($link->route_params) { + $params = is_string($link->route_params) + ? json_decode($link->route_params, true) + : $link->route_params; + if ($params && is_array($params)) { + $formatted['params'] = $params; + } + } + } else { + return null; + } + break; + case 'path': + $formatted['direct_url'] = $link->custom_path; + break; + case 'url': + $formatted['direct_url'] = $link->custom_url; + break; + } + + // Handle children (dropdown items) + if ($link->children && $link->children->count() > 0) { + $children = []; + foreach ($link->children->where('is_active', true) as $child) { + if ($this->shouldShowLink($child)) { + $childFormatted = $this->formatLink($child); + if ($childFormatted) { + $children[] = $childFormatted; + } + } + } + if (!empty($children)) { + $formatted['children'] = $children; + } + } + + return $formatted; + } catch (\Exception $e) { + \Log::error('NavbarManager: Error formatting link', [ + 'link_id' => $link->id ?? 'unknown', + 'error' => $e->getMessage() + ]); + return null; + } + } +} diff --git a/extensions/Others/NavbarManager/resources/views/components/custom-link.blade.php b/extensions/Others/NavbarManager/resources/views/components/custom-link.blade.php new file mode 100644 index 000000000..8228b6401 --- /dev/null +++ b/extensions/Others/NavbarManager/resources/views/components/custom-link.blade.php @@ -0,0 +1,34 @@ +@props(['link', 'class' => '']) + +@php + $href = ''; + $spa = true; + + // Determine the href based on link type + if (isset($link['direct_url'])) { + // Custom path or external URL + $href = $link['direct_url']; + $spa = false; // Disable SPA for custom URLs + } elseif (isset($link['route'])) { + // Laravel route + try { + $href = route($link['route'], $link['params'] ?? []); + } catch (\Exception $e) { + // If route doesn't exist, skip this link + $href = '#'; + } + } else { + $href = '#'; // Fallback + } +@endphp + +@if($href !== '#') +merge(['class' => 'flex flex-row items-center gap-2 text-sm font-semibold text-wrap ' . $class . ' ' . ($href === request()->url() ? 'text-primary' : 'text-base hover:text-base/80')]) }} + @if($spa) wire:navigate @endif> + @if(isset($link['icon'])) + + @endif + {{ $link['name'] }} + +@endif diff --git a/extensions/Others/NavbarManager/resources/views/components/navigation-links.blade.php b/extensions/Others/NavbarManager/resources/views/components/navigation-links.blade.php new file mode 100644 index 000000000..476707e9a --- /dev/null +++ b/extensions/Others/NavbarManager/resources/views/components/navigation-links.blade.php @@ -0,0 +1,31 @@ +@props(['links' => []]) + +@if(count($links) > 0) + @foreach($links as $nav) + @if (isset($nav['children']) && count($nav['children']) > 0) + {{-- Dropdown link --}} +
+ + +
+ + @if(isset($nav['icon'])) + + @endif + {{ $nav['name'] }} + +
+
+ + @foreach ($nav['children'] as $child) + + @endforeach + +
+
+ @else + {{-- Single link --}} + + @endif + @endforeach +@endif diff --git a/extensions/Others/NavbarManager/routes/web.php b/extensions/Others/NavbarManager/routes/web.php new file mode 100644 index 000000000..143a81b72 --- /dev/null +++ b/extensions/Others/NavbarManager/routes/web.php @@ -0,0 +1,25 @@ + 'navbar'], function () { + Route::get('/redirect/{linkId}', function($linkId) { + try { + $link = NavbarLink::findOrFail($linkId); + if (!$link->is_active) { + abort(404); + } + + if ($link->link_type === 'path' && $link->custom_path) { + return redirect($link->custom_path); + } elseif ($link->link_type === 'url' && $link->custom_url) { + return redirect($link->custom_url); + } + + abort(404); + } catch (\Exception $e) { + abort(404); + } + })->name('navbar.redirect'); +}); diff --git a/public/assets/images/bg.svg b/public/assets/images/bg.svg new file mode 100644 index 000000000..ee1bb6638 --- /dev/null +++ b/public/assets/images/bg.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/public/assets/images/bg_min.png b/public/assets/images/bg_min.png new file mode 100644 index 000000000..5ff773b6c Binary files /dev/null and b/public/assets/images/bg_min.png differ diff --git a/public/assets/images/bggrad.jpg b/public/assets/images/bggrad.jpg new file mode 100644 index 000000000..8a70fd098 Binary files /dev/null and b/public/assets/images/bggrad.jpg differ diff --git a/public/assets/images/bgwires.jpeg b/public/assets/images/bgwires.jpeg new file mode 100644 index 000000000..768f5ec30 Binary files /dev/null and b/public/assets/images/bgwires.jpeg differ diff --git a/public/assets/images/bgwires.png b/public/assets/images/bgwires.png new file mode 100644 index 000000000..8d7a432f9 Binary files /dev/null and b/public/assets/images/bgwires.png differ diff --git a/public/assets/images/cpu.png b/public/assets/images/cpu.png new file mode 100644 index 000000000..805798d37 Binary files /dev/null and b/public/assets/images/cpu.png differ diff --git a/public/assets/images/logo.webp b/public/assets/images/logo.webp new file mode 100644 index 000000000..47c7c3811 Binary files /dev/null and b/public/assets/images/logo.webp differ diff --git a/public/assets/images/networkbg.webp b/public/assets/images/networkbg.webp new file mode 100644 index 000000000..21043a8b9 Binary files /dev/null and b/public/assets/images/networkbg.webp differ diff --git a/public/assets/images/webbg.webp b/public/assets/images/webbg.webp new file mode 100644 index 000000000..956d11b0f Binary files /dev/null and b/public/assets/images/webbg.webp differ diff --git a/public/assets/videos/demo.mp4 b/public/assets/videos/demo.mp4 new file mode 100644 index 000000000..9d1690c7d Binary files /dev/null and b/public/assets/videos/demo.mp4 differ