diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index 4ad3b9b29..ed454bb77 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -102,6 +102,36 @@ public function loadServices()
: asset($default_logo),
] + (array) $service;
})->all();
+
+ // Extract unique categories from services
+ $categories = collect($services)
+ ->pluck('category')
+ ->filter()
+ ->unique()
+ ->map(function ($category) {
+ // Handle multiple categories separated by comma
+ if (str_contains($category, ',')) {
+ return collect(explode(',', $category))->map(fn ($cat) => trim($cat));
+ }
+
+ return [$category];
+ })
+ ->flatten()
+ ->unique()
+ ->map(function ($category) {
+ // Format common acronyms to uppercase
+ $acronyms = ['ai', 'api', 'ci', 'cd', 'cms', 'crm', 'erp', 'iot', 'vpn', 'vps', 'dns', 'ssl', 'tls', 'ssh', 'ftp', 'http', 'https', 'smtp', 'imap', 'pop3', 'sql', 'nosql', 'json', 'xml', 'yaml', 'csv', 'pdf', 'sms', 'mfa', '2fa', 'oauth', 'saml', 'jwt', 'rest', 'soap', 'grpc', 'graphql', 'websocket', 'webrtc', 'p2p', 'b2b', 'b2c', 'seo', 'sem', 'ppc', 'roi', 'kpi', 'ui', 'ux', 'ide', 'sdk', 'api', 'cli', 'gui', 'cdn', 'ddos', 'dos', 'xss', 'csrf', 'sqli', 'rce', 'lfi', 'rfi', 'ssrf', 'xxe', 'idor', 'owasp', 'gdpr', 'hipaa', 'pci', 'dss', 'iso', 'nist', 'cve', 'cwe', 'cvss'];
+ $lower = strtolower($category);
+
+ if (in_array($lower, $acronyms)) {
+ return strtoupper($category);
+ }
+
+ return $category;
+ })
+ ->sort(SORT_NATURAL | SORT_FLAG_CASE)
+ ->values()
+ ->all();
$gitBasedApplications = [
[
'id' => 'public',
@@ -202,6 +232,7 @@ public function loadServices()
return [
'services' => $services,
+ 'categories' => $categories,
'gitBasedApplications' => $gitBasedApplications,
'dockerBasedApplications' => $dockerBasedApplications,
'databases' => $databases,
diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php
index f8925c1ce..277ca99af 100644
--- a/resources/views/livewire/project/new/select.blade.php
+++ b/resources/views/livewire/project/new/select.blade.php
@@ -13,9 +13,55 @@
@if ($current_step === 'type')
-
+
+
+
+
+
+
+
Filter by category
+
+
+
+
{ if (openCategoryDropdown) $refs.categorySearchInput.focus() })"
+ class="flex items-center justify-between gap-2 py-1.5 px-3 w-64 text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-pointer hover:ring-coolgray-400 transition-all whitespace-nowrap">
+
+
+
+
+
+
+
Loading...
@@ -140,6 +186,8 @@ function sortFn(a, b) {
function searchResources() {
return {
search: '',
+ selectedCategory: '',
+ categories: [],
loading: false,
isSticky: false,
selecting: false,
@@ -156,11 +204,13 @@ function searchResources() {
this.loading = true;
const {
services,
+ categories,
gitBasedApplications,
dockerBasedApplications,
databases
} = await this.$wire.loadServices();
this.services = services;
+ this.categories = categories || [];
this.gitBasedApplications = gitBasedApplications;
this.dockerBasedApplications = dockerBasedApplications;
this.databases = databases;
@@ -171,15 +221,30 @@ function searchResources() {
},
filterAndSort(items, isSort = true) {
const searchLower = this.search.trim().toLowerCase();
+ let filtered = Object.values(items);
- if (searchLower === '') {
- return isSort ? Object.values(items).sort(sortFn) : Object.values(items);
+ // Filter by category if selected
+ if (this.selectedCategory !== '') {
+ const selectedCategoryLower = this.selectedCategory.toLowerCase();
+ filtered = filtered.filter(item => {
+ if (!item.category) return false;
+ // Handle comma-separated categories
+ const categories = item.category.includes(',')
+ ? item.category.split(',').map(c => c.trim().toLowerCase())
+ : [item.category.toLowerCase()];
+ return categories.includes(selectedCategoryLower);
+ });
}
- const filtered = Object.values(items).filter(item => {
- return (item.name?.toLowerCase().includes(searchLower) ||
- item.description?.toLowerCase().includes(searchLower) ||
- item.slogan?.toLowerCase().includes(searchLower))
- })
+
+ // Filter by search term
+ if (searchLower !== '') {
+ filtered = filtered.filter(item => {
+ return (item.name?.toLowerCase().includes(searchLower) ||
+ item.description?.toLowerCase().includes(searchLower) ||
+ item.slogan?.toLowerCase().includes(searchLower))
+ });
+ }
+
return isSort ? filtered.sort(sortFn) : filtered;
},
get filteredGitBasedApplications() {