@@ -19,7 +19,12 @@ const showNotifications = ref(false);
1919const notifications = ref ([]);
2020const unreadCount = ref (user .value ? .notification_unread_count ?? 0 );
2121const isLoadingNotifications = ref (false );
22+ const globalSearchQuery = ref (' ' );
23+ const globalSearchResults = ref ([]);
24+ const showGlobalSearchResults = ref (false );
25+ const isGlobalSearchLoading = ref (false );
2226let notificationsPoller = null ;
27+ let globalSearchDebounce = null ;
2328
2429function applyTheme (nextTheme ) {
2530 const root = document .documentElement ;
@@ -107,6 +112,58 @@ function toggleNotifications() {
107112 }
108113}
109114
115+ async function runGlobalSearch () {
116+ const query = globalSearchQuery .value .trim ();
117+
118+ if (query .length < 2 ) {
119+ globalSearchResults .value = [];
120+ showGlobalSearchResults .value = false ;
121+ return ;
122+ }
123+
124+ isGlobalSearchLoading .value = true ;
125+
126+ try {
127+ const response = await window .axios .get (route (' search.global' ), {
128+ params: { q: query },
129+ });
130+
131+ globalSearchResults .value = response .data ? .data ?? [];
132+ showGlobalSearchResults .value = true ;
133+ } catch {
134+ globalSearchResults .value = [];
135+ showGlobalSearchResults .value = true ;
136+ } finally {
137+ isGlobalSearchLoading .value = false ;
138+ }
139+ }
140+
141+ function onGlobalSearchInput () {
142+ if (globalSearchDebounce !== null ) {
143+ clearTimeout (globalSearchDebounce);
144+ }
145+
146+ globalSearchDebounce = setTimeout (() => {
147+ runGlobalSearch ();
148+ }, 220 );
149+ }
150+
151+ function openGlobalSearchResult (result ) {
152+ globalSearchQuery .value = ' ' ;
153+ globalSearchResults .value = [];
154+ showGlobalSearchResults .value = false ;
155+
156+ if (result? .url ) {
157+ router .visit (result .url );
158+ }
159+ }
160+
161+ function hideGlobalSearchResults () {
162+ setTimeout (() => {
163+ showGlobalSearchResults .value = false ;
164+ }, 120 );
165+ }
166+
110167function logout () {
111168 router .post (route (' logout' ));
112169}
@@ -132,6 +189,10 @@ onBeforeUnmount(() => {
132189 if (notificationsPoller !== null ) {
133190 clearInterval (notificationsPoller);
134191 }
192+
193+ if (globalSearchDebounce !== null ) {
194+ clearTimeout (globalSearchDebounce);
195+ }
135196});
136197
137198watch (theme, (nextTheme ) => {
@@ -188,6 +249,47 @@ watch(theme, (nextTheme) => {
188249
189250 <!-- User Menu -->
190251 < div class = " hidden sm:flex sm:items-center sm:space-x-4" >
252+ < div class = " relative" >
253+ < input
254+ v- model= " globalSearchQuery"
255+ type= " search"
256+ placeholder= " Search #, project, request, submittal..."
257+ class = " w-80 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/40 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
258+ @input= " onGlobalSearchInput"
259+ @focus= " onGlobalSearchInput"
260+ @blur= " hideGlobalSearchResults"
261+ / >
262+
263+ < div
264+ v- if = " showGlobalSearchResults"
265+ class = " absolute right-0 z-50 mt-2 w-[28rem] overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-slate-700 dark:bg-slate-900"
266+ >
267+ < div
268+ v- if = " isGlobalSearchLoading"
269+ class = " px-4 py-3 text-sm text-gray-500 dark:text-slate-300"
270+ >
271+ Searching...
272+ < / div>
273+ < div
274+ v- else - if = " globalSearchResults.length === 0"
275+ class = " px-4 py-3 text-sm text-gray-500 dark:text-slate-300"
276+ >
277+ No matches found.
278+ < / div>
279+ < button
280+ v- for = " result in globalSearchResults"
281+ : key= " `${result.type}-${result.url}`"
282+ type= " button"
283+ class = " block w-full border-b border-gray-100 px-4 py-3 text-left last:border-b-0 hover:bg-gray-50 dark:border-slate-800 dark:hover:bg-slate-800"
284+ @mousedown .prevent = " openGlobalSearchResult(result)"
285+ >
286+ < p class = " text-sm font-medium text-gray-900 dark:text-slate-100" > {{ result .title }}< / p>
287+ < p class = " mt-1 text-xs text-gray-500 dark:text-slate-400" >
288+ {{ result .type .replace (' _' , ' ' ) }}< span v- if = " result.subtitle" > · {{ result .subtitle }}< / span>
289+ < / p>
290+ < / button>
291+ < / div>
292+ < / div>
191293 < div class = " relative" >
192294 < button
193295 type= " button"
0 commit comments