diff --git a/app/Livewire/Header.php b/app/Livewire/Header.php index f849e850..a5577314 100644 --- a/app/Livewire/Header.php +++ b/app/Livewire/Header.php @@ -7,6 +7,7 @@ use App\Enums\UserRole; use App\Models\Project; use Livewire\Component; +use Livewire\Attributes\On; use Filament\Actions\Action; use App\Rules\ProfanityCheck; use App\Settings\GeneralSettings; @@ -44,29 +45,23 @@ public function mount() ->get(); } - public function render() + #[On('create-item-from-search')] + public function openSubmitItemWithQuery(string $query): void { - return view('livewire.header'); + // Mount the submit item action with the query as an argument + $this->mountAction('submitItem', ['prefilledTitle' => $query]); } - public function searchItemAction(): Action + #[On('open-submit-item-modal')] + public function openSubmitItemModal(): void { - return Action::make('searchItem') - ->link() - ->color('gray') - ->label(function () { - return view('components.search-button-label'); - }) - ->icon('heroicon-o-magnifying-glass') - ->extraAttributes(['class' => '!px-3 !py-1.5 rounded-lg text-white hover:text-white hover:bg-white/10 focus:ring-white/20 [&_svg]:text-white']) - ->modalWidth('4xl') - ->modalFooterActions([ - Action::make('👀')->hidden() - ]) - ->modalHeading($this->getRandomFunnyPlaceholder()) - ->modalIcon('heroicon-o-magnifying-glass') - ->modalAlignment(Alignment::Left) - ->modalContent(view('modals.search')); + // Mount the submit item action without pre-filled data + $this->mountAction('submitItem'); + } + + public function render() + { + return view('livewire.header'); } public function submitItemAction(): Action @@ -101,8 +96,23 @@ public function submitItemAction(): Action ->modalIcon('heroicon-o-plus-circle') ->modalWidth('3xl') ->modalSubmitActionLabel('Confirm') - ->fillForm(function () { - return $this->currentProjectId ? ['project_id' => $this->currentProjectId] : []; + ->mountUsing(function ($form, array $arguments) { + // Fill the form with prefilled data if available + $data = []; + + // Add project_id if currentProjectId is set + if ($this->currentProjectId) { + $data['project_id'] = $this->currentProjectId; + } + + // Add prefilled title if provided from search + if ($prefilledTitle = $arguments['prefilledTitle'] ?? null) { + $data['title'] = $prefilledTitle; + // Also set similar items for the prefilled title + $this->setSimilarItems($prefilledTitle); + } + + $form->fill($data); }) ->schema(function () { $inputs = []; @@ -215,28 +225,4 @@ public function setSimilarItems($state): void return $query; })->get(['title', 'slug']) : collect([]); } - - protected function getRandomFunnyPlaceholder(): string - { - $placeholders = [ - "Type here to find your lost keys... or your sanity", - "Searching for Wi-Fi signals and lost socks", - "Type something brilliant or your cat will judge you", - "Looking for answers, memes, and cat videos", - "Search for unicorns, we might find one", - "Finding a needle in a digital haystack", - "Type your thoughts, we'll pretend to understand", - "Searching for dinosaurs in the digital age", - "Lost in the web? We've got a virtual map", - "Hunting for pixels and easter eggs", - "Type something epic or order pizza, your choice", - "Lost in the code? We'll be your debugger", - "Looking for a shortcut to success", - "Searching for the meaning of life... or cute cat videos" - ]; - - shuffle($placeholders); - - return $placeholders[0]; - } } diff --git a/app/Livewire/SpotlightSearch.php b/app/Livewire/SpotlightSearch.php new file mode 100644 index 00000000..0d527b0c --- /dev/null +++ b/app/Livewire/SpotlightSearch.php @@ -0,0 +1,117 @@ +isOpen = true; + $this->query = ''; + $this->items = []; + $this->projects = []; + $this->totalResults = 0; + + // Dispatch browser event for Alpine.js + $this->dispatch('spotlight-opened'); + } + + public function close(): void + { + $this->isOpen = false; + $this->query = ''; + $this->items = []; + $this->projects = []; + $this->totalResults = 0; + } + + public function updatedQuery(): void + { + if (empty(trim($this->query))) { + $this->items = []; + $this->projects = []; + $this->totalResults = 0; + return; + } + + $this->searchItems(); + $this->searchProjects(); + $this->totalResults = count($this->items) + count($this->projects); + } + + protected function searchItems(): void + { + $query = Item::query() + ->visibleForCurrentUser() + ->with(['project', 'board', 'user']) + ->where(function ($q) { + $q->where('title', 'like', '%' . $this->query . '%') + ->orWhere('content', 'like', '%' . $this->query . '%'); + }) + ->orderByDesc('created_at') + ->limit(8); + + $this->items = $query->get()->map(function (Item $item) { + return [ + 'id' => $item->id, + 'title' => $item->title, + 'slug' => $item->slug, + 'project_title' => $item->project?->title, + 'board_title' => $item->board?->title, + 'votes_count' => $item->total_votes ?? 0, + 'created_at' => $item->created_at?->diffForHumans(), + 'url' => route('items.show', $item), + ]; + })->toArray(); + } + + protected function searchProjects(): void + { + $query = Project::query() + ->visibleForCurrentUser() + ->with(['boards']) + ->where(function ($q) { + $q->where('title', 'like', '%' . $this->query . '%') + ->orWhere('description', 'like', '%' . $this->query . '%'); + }) + ->orderBy('title') + ->limit(8); + + $this->projects = $query->get()->map(function (Project $project) { + return [ + 'id' => $project->id, + 'title' => $project->title, + 'slug' => $project->slug, + 'description' => $project->description, + 'icon' => $project->icon, + 'boards_count' => $project->boards->count(), + 'url' => route('projects.show', $project), + ]; + })->toArray(); + } + + public function createNewItem(): void + { + $query = $this->query; + $this->close(); + $this->dispatch('create-item-from-search', query: $query); + } + + public function render() + { + return view('livewire.spotlight-search'); + } +} diff --git a/lang/en/spotlight.php b/lang/en/spotlight.php index efe117fb..ce2edac5 100644 --- a/lang/en/spotlight.php +++ b/lang/en/spotlight.php @@ -2,6 +2,7 @@ return [ 'search' => 'Search', + 'search_button' => 'Search', 'create-item' => [ 'name' => 'Create item', 'description' => 'Create an item', @@ -18,4 +19,26 @@ 'name' => 'Logout', 'description' => 'Logout out of your account', ], + + // Search Interface + 'search_placeholder' => 'Search items and projects...', + 'section_items' => 'Items', + 'section_projects' => 'Projects', + 'section_actions' => 'Actions', + 'quick_actions' => 'Quick Actions', + 'create_new_item' => 'Create new item', + 'create_new_item_desc' => 'Create a new roadmap item', + 'profile_title' => 'Profile', + 'profile_desc' => 'View and edit your profile', + 'activity' => 'Activity', + 'activity_desc' => 'View recent activity and updates', + 'admin_panel' => 'Admin Panel', + 'admin_panel_desc' => 'Manage your roadmap settings', + 'no_results' => 'No results found for', + 'initial_message' => 'Search for items and projects', + 'keyboard_hint' => 'Use arrow keys to navigate', + 'keyboard_navigate' => 'to navigate', + 'keyboard_open' => 'to open', + 'results_count' => ':count result|:count results', + 'board' => 'board|boards', ]; diff --git a/lang/nl/spotlight.php b/lang/nl/spotlight.php index c5bd457e..b15b1214 100644 --- a/lang/nl/spotlight.php +++ b/lang/nl/spotlight.php @@ -2,6 +2,7 @@ return [ 'search' => 'Zoeken', + 'search_button' => 'Zoeken', 'create-item' => [ 'name' => 'Item aanmaken', 'description' => 'Maak een item aan', @@ -18,4 +19,26 @@ 'name' => 'Uitloggen', 'description' => 'Log jezelf uit', ], + + // Search Interface + 'search_placeholder' => 'Zoek items en projecten...', + 'section_items' => 'Items', + 'section_projects' => 'Projecten', + 'section_actions' => 'Acties', + 'quick_actions' => 'Snelle Acties', + 'create_new_item' => 'Nieuw item aanmaken', + 'create_new_item_desc' => 'Maak een nieuw roadmap item aan', + 'profile_title' => 'Profiel', + 'profile_desc' => 'Bekijk en bewerk je profiel', + 'activity' => 'Activiteit', + 'activity_desc' => 'Bekijk recente activiteit en updates', + 'admin_panel' => 'Beheerpaneel', + 'admin_panel_desc' => 'Beheer je roadmap instellingen', + 'no_results' => 'Geen resultaten gevonden voor', + 'initial_message' => 'Zoek op items en projecten', + 'keyboard_hint' => 'Gebruik de pijltjestoetsen om te navigeren', + 'keyboard_navigate' => 'om te navigeren', + 'keyboard_open' => 'om te openen', + 'results_count' => ':count resultaat|:count resultaten', + 'board' => 'board|boards', ]; diff --git a/resources/css/app.css b/resources/css/app.css index 4236955d..967b5a52 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -134,3 +134,42 @@ .fi-ta { @apply w-full; } + +/* Spotlight Search Styles */ +.spotlight-container { + box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); +} + +@media (prefers-color-scheme: dark) { + .spotlight-container { + box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.5); + } +} + +/* Custom scrollbar for spotlight results */ +.spotlight-container .max-h-\[60vh\]::-webkit-scrollbar { + width: 8px; +} + +.spotlight-container .max-h-\[60vh\]::-webkit-scrollbar-track { + background: transparent; +} + +.spotlight-container .max-h-\[60vh\]::-webkit-scrollbar-thumb { + background: rgba(156, 163, 175, 0.3); + border-radius: 4px; +} + +.spotlight-container .max-h-\[60vh\]::-webkit-scrollbar-thumb:hover { + background: rgba(156, 163, 175, 0.5); +} + +@media (prefers-color-scheme: dark) { + .spotlight-container .max-h-\[60vh\]::-webkit-scrollbar-thumb { + background: rgba(75, 85, 99, 0.5); + } + + .spotlight-container .max-h-\[60vh\]::-webkit-scrollbar-thumb:hover { + background: rgba(75, 85, 99, 0.7); + } +} diff --git a/resources/views/components/app.blade.php b/resources/views/components/app.blade.php index 2e44aadb..5c8ade63 100644 --- a/resources/views/components/app.blade.php +++ b/resources/views/components/app.blade.php @@ -97,6 +97,7 @@ function updateTheme() { @livewireScriptConfig @filamentScripts @livewire('notifications') +@livewire('spotlight-search') @stack('javascript') diff --git a/resources/views/components/search-button-label.blade.php b/resources/views/components/search-button-label.blade.php deleted file mode 100644 index 537b7151..00000000 --- a/resources/views/components/search-button-label.blade.php +++ /dev/null @@ -1,6 +0,0 @@ - - {{ trans('spotlight.search') }} - - diff --git a/resources/views/components/spotlight/result-item.blade.php b/resources/views/components/spotlight/result-item.blade.php new file mode 100644 index 00000000..f8010402 --- /dev/null +++ b/resources/views/components/spotlight/result-item.blade.php @@ -0,0 +1,87 @@ +@props([ + 'href' => null, + 'index', + 'icon', + 'iconClass' => '', + 'title', + 'meta' => null, + 'aside' => null, + 'navigate' => true, + 'closeOnClick' => true, +]) + +@php + $baseClasses = 'group flex items-start gap-3 rounded-lg px-3 py-2.5 text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-800'; + $selectedClasses = 'bg-gray-100 dark:bg-gray-800'; + + $iconBaseClasses = 'mt-0.5 h-5 w-5 flex-shrink-0'; + $iconDefaultClasses = 'text-gray-400 group-hover:text-blue-500 dark:text-gray-500'; + $iconSelectedClasses = 'text-blue-500 dark:text-blue-400'; +@endphp + +@if($href) + + @if($iconClass) + {{ $icon }} + @else + + @endif +
+
+ {{ $title }} +
+ @if($slot->isNotEmpty()) + {{ $slot }} + @elseif($meta) +
+ {{ $meta }} +
+ @endif +
+ @if($aside) + {{ $aside }} + @endif +
+@else + +@endif \ No newline at end of file diff --git a/resources/views/components/spotlight/section-header.blade.php b/resources/views/components/spotlight/section-header.blade.php new file mode 100644 index 00000000..77df823f --- /dev/null +++ b/resources/views/components/spotlight/section-header.blade.php @@ -0,0 +1,5 @@ +@props(['title']) + +
+ {{ $title }} +
\ No newline at end of file diff --git a/resources/views/livewire/header.blade.php b/resources/views/livewire/header.blade.php index a4b51218..7fe1f16e 100644 --- a/resources/views/livewire/header.blade.php +++ b/resources/views/livewire/header.blade.php @@ -1,7 +1,7 @@
+ @keydown.window.cmd.k.prevent="$dispatch('open-spotlight')" + @keydown.window.ctrl.k.prevent="$dispatch('open-spotlight')">