11"use client" ;
2- import React , { useState , useEffect } from "react" ;
2+ import React , { useState , useEffect , useRef } from "react" ;
33import BBIcon from "./components/theme/Icon/bbIcon" ;
44import DevelopmentToolsStyles from "./developmentToolsStyles.module.scss" ;
55import { InputField } from "./components/theme/form/formFeildComponent" ;
@@ -16,6 +16,39 @@ import ConvertXIcon from "./components/theme/Icon/convertXIcon";
1616import GenieIcon from "./components/theme/Icon/genieIcon" ;
1717import DevUtilsIcon from "./components/theme/Icon/devUtilsIcon" ;
1818
19+ const escapeRegex = ( str : string ) =>
20+ str . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
21+
22+ const HighlightText = ( {
23+ text,
24+ searchTerm,
25+ className,
26+ } : {
27+ text : string ;
28+ searchTerm : string ;
29+ className ?: string ;
30+ } ) => {
31+ if ( ! searchTerm ?. trim ( ) ) return < span className = { className } > { text } </ span > ;
32+ const regex = new RegExp ( `(${ escapeRegex ( searchTerm ) } )` , "gi" ) ;
33+ const parts = text . split ( regex ) ;
34+ return (
35+ < span className = { className } >
36+ { parts . map ( ( part , i ) =>
37+ part . toLowerCase ( ) === searchTerm . toLowerCase ( ) ? (
38+ < mark
39+ key = { i }
40+ className = { DevelopmentToolsStyles . searchHighlight }
41+ >
42+ { part }
43+ </ mark >
44+ ) : (
45+ < React . Fragment key = { i } > { part } </ React . Fragment >
46+ )
47+ ) }
48+ </ span >
49+ ) ;
50+ } ;
51+
1952const CATEGORY_GROUPS = [
2053 "Text Lab" ,
2154 "Code Forge" ,
@@ -99,7 +132,7 @@ const classifyBasis = (title: string, url: string): BasisType => {
99132 return "Converters" ;
100133
101134 if ( t . includes ( "generator" ) || t . includes ( "random" ) ) return "Generators" ;
102-
135+
103136 if ( t . includes ( "color" ) || t . includes ( "image" ) ) return "Color/Image" ;
104137
105138 if (
@@ -130,6 +163,7 @@ const Page = () => {
130163 const [ selectedBasis , setSelectedBasis ] = useState < BasisType > ( "All" ) ;
131164 const [ favorites , setFavorites ] = useState < string [ ] > ( [ ] ) ;
132165 const [ showFavoritesOnly , setShowFavoritesOnly ] = useState ( false ) ;
166+ const searchInputRef = useRef < HTMLInputElement > ( null ) ;
133167
134168 useEffect ( ( ) => {
135169 try {
@@ -152,6 +186,27 @@ const Page = () => {
152186 }
153187 } , [ ] ) ;
154188
189+ // Keyboard shortcut: Ctrl/Cmd + K to focus search
190+ useEffect ( ( ) => {
191+ const handler = ( e : KeyboardEvent ) => {
192+ if ( ( e . metaKey || e . ctrlKey ) && e . key === "k" ) {
193+ e . preventDefault ( ) ;
194+ const input = document . getElementById ( "txtSearch" ) as HTMLInputElement ;
195+ if ( input ) {
196+ input . focus ( ) ;
197+ input . scrollIntoView ( { behavior : "smooth" , block : "center" } ) ;
198+ }
199+ }
200+ // Escape to blur/clear search
201+ if ( e . key === "Escape" && document . activeElement ?. id === "txtSearch" ) {
202+ e . preventDefault ( ) ;
203+ ( document . activeElement as HTMLElement ) ?. blur ( ) ;
204+ }
205+ } ;
206+ window . addEventListener ( "keydown" , handler ) ;
207+ return ( ) => window . removeEventListener ( "keydown" , handler ) ;
208+ } , [ ] ) ;
209+
155210 const toggleFavorite = ( e : React . MouseEvent , url : string ) => {
156211 e . preventDefault ( ) ;
157212 e . stopPropagation ( ) ;
@@ -192,13 +247,15 @@ const Page = () => {
192247 } ) ) ;
193248
194249 let filteredItems = itemsWithMeta
195- . filter ( ( item ) =>
196- searchTerm
197- ? ( item ?. title || "" )
198- . toLowerCase ( )
199- . includes ( searchTerm . toLowerCase ( ) )
200- : true
201- )
250+ . filter ( ( item ) => {
251+ if ( ! searchTerm ) return true ;
252+ const t = searchTerm . toLowerCase ( ) ;
253+ return (
254+ ( item ?. title || "" ) . toLowerCase ( ) . includes ( t ) ||
255+ ( item ?. description || "" ) . toLowerCase ( ) . includes ( t ) ||
256+ ( item ?. url || "" ) . toLowerCase ( ) . includes ( t )
257+ ) ;
258+ } )
202259 . filter ( ( item ) => ( selectedCategory ? item . __group === selectedCategory : true ) )
203260 . filter ( ( item ) => ( selectedBasis === "All" ? true : item . __basis === selectedBasis ) ) ;
204261
@@ -271,7 +328,8 @@ const Page = () => {
271328 />
272329 </ div >
273330 ) : (
274- < div className = "absolute lg:right-[210px] lg:top-4 right-11 top-4 2xl:right-[13rem] 2xl:top-4" >
331+ < div className = "absolute lg:right-[210px] lg:top-4 right-11 top-4 2xl:right-[13rem] 2xl:top-4 flex items-center gap-2" >
332+ < span className = { DevelopmentToolsStyles . keyboardHint } > Ctrl K</ span >
275333 < SearchIcon className = "text-white" />
276334 </ div >
277335 ) }
@@ -330,11 +388,10 @@ const Page = () => {
330388 < button
331389 key = { cat }
332390 onClick = { ( ) => setSelectedCategory ( ( prev ) => ( prev === cat ? null : cat ) ) }
333- className = { `w-full text-left px-3 py-2 rounded-lg border transition ${
334- selectedCategory === cat
335- ? "bg-primary text-black font-bold border-primary"
336- : "bg-black/40 text-white border-[#222] hover:bg-black/50"
337- } `}
391+ className = { `w-full text-left px-3 py-2 rounded-lg border transition ${ selectedCategory === cat
392+ ? "bg-primary text-black font-bold border-primary"
393+ : "bg-black/40 text-white border-[#222] hover:bg-black/50"
394+ } `}
338395 >
339396 < div className = "flex items-center justify-between" >
340397 < span className = "text-sm flex items-center gap-2" >
@@ -362,12 +419,11 @@ const Page = () => {
362419 < button
363420 key = { b }
364421 onClick = { ( ) => setSelectedBasis ( b ) }
365- className = { `px-2.5 py-1.5 rounded-full text-xs border transition ${
366- selectedBasis === b
367- ? "bg-primary text-black font-bold border-primary"
368- : "bg-black/40 text-white border-[#222] hover:bg-black/50"
369- } `}
370- aria-pressed = { selectedBasis === b }
422+ className = { `px-2.5 py-1.5 rounded-full text-xs border transition ${ selectedBasis === b
423+ ? "bg-primary text-black font-bold border-primary"
424+ : "bg-black/40 text-white border-[#222] hover:bg-black/50"
425+ } `}
426+ aria-pressed = { selectedBasis === b ? "true" : "false" }
371427 >
372428 < span className = "flex items-center gap-1.5" >
373429 < span > { b } </ span >
@@ -426,7 +482,9 @@ const Page = () => {
426482 className = { `bg-white/5 rounded-lg p-8 w-full ${ DevelopmentToolsStyles . contentCardHoverEffect } group md:min-h-[160px] relative` }
427483 >
428484 < div className = "flex justify-between items-start gap-2" >
429- < h3 className = "text-lg font-semibold pr-6" > { item ?. title } </ h3 >
485+ < h3 className = "text-lg font-semibold pr-6" >
486+ < HighlightText text = { item ?. title || "" } searchTerm = { searchTerm } />
487+ </ h3 >
430488 < button
431489 onClick = { ( e ) => toggleFavorite ( e , item ?. url ) }
432490 className = "absolute right-4 top-4 text-white/30 hover:text-yellow-400 transition-colors z-10"
@@ -455,23 +513,9 @@ const Page = () => {
455513 ? description . slice ( 0 , 50 ) + "..."
456514 : description ;
457515
458- return truncated
459- . split ( "BetterBugs.io" )
460- . map ( ( part : any , i : any , arr : any ) => (
461- < React . Fragment key = { i } >
462- { part }
463- { i !== arr . length - 1 && (
464- < a
465- href = "https://BetterBugs.io"
466- target = "_blank"
467- rel = "noopener noreferrer"
468- className = "text-primary group-hover:underline group-hover:text-secondary group-hover:font-semibold"
469- >
470- BetterBugs.io
471- </ a >
472- ) }
473- </ React . Fragment >
474- ) ) ;
516+ return (
517+ < HighlightText text = { truncated } searchTerm = { searchTerm } />
518+ ) ;
475519 } ) ( ) }
476520 </ p >
477521 < div className = "mt-3 text-xs text-white/50" > { item ?. __group } • { item ?. __basis } </ div >
0 commit comments