|
| 1 | +import React, { useState, useEffect, useRef } from 'react'; |
| 2 | +import Fuse from 'fuse.js'; |
| 3 | +import { format } from 'date-fns'; |
| 4 | +import { es } from 'date-fns/locale'; |
| 5 | +import CopyButton from './CopyButton'; |
| 6 | +import StoryGenerator from './StoryGenerator'; |
| 7 | + |
| 8 | +interface SearchItem { |
| 9 | + id: string; |
| 10 | + title: string; |
| 11 | + description: string; |
| 12 | + tags: string[]; |
| 13 | + date: string; |
| 14 | + type: 'post' | 'frase'; |
| 15 | + url: string; |
| 16 | + slug?: string; |
| 17 | +} |
| 18 | + |
| 19 | +const HighlightText = ({ text, query }: { text: string; query: string }) => { |
| 20 | + if (!query) return <>{text}</>; |
| 21 | + const parts = text.split(new RegExp(`(${query})`, 'gi')); |
| 22 | + return ( |
| 23 | + <> |
| 24 | + {parts.map((part, i) => |
| 25 | + part.toLowerCase() === query.toLowerCase() ? ( |
| 26 | + <mark key={i} className="bg-lime-400/20 text-lime-400 px-1 rounded inline-block bg-transparent">{part}</mark> |
| 27 | + ) : ( |
| 28 | + part |
| 29 | + ) |
| 30 | + )} |
| 31 | + </> |
| 32 | + ); |
| 33 | +}; |
| 34 | + |
| 35 | +export default function BuscarPage({ items }: { items: SearchItem[] }) { |
| 36 | + const [query, setQuery] = useState(''); |
| 37 | + const [activeTab, setActiveTab] = useState<'posts' | 'frases'>('posts'); |
| 38 | + const [results, setResults] = useState<SearchItem[]>([]); |
| 39 | + const fuseRef = useRef<Fuse<SearchItem> | null>(null); |
| 40 | + |
| 41 | + useEffect(() => { |
| 42 | + // Read query from URL on mount |
| 43 | + const params = new URLSearchParams(window.location.search); |
| 44 | + const q = params.get('q'); |
| 45 | + if (q) setQuery(q); |
| 46 | + }, []); |
| 47 | + |
| 48 | + useEffect(() => { |
| 49 | + // Update URL when query changes |
| 50 | + const url = new URL(window.location.href); |
| 51 | + if (query) { |
| 52 | + url.searchParams.set('q', query); |
| 53 | + } else { |
| 54 | + url.searchParams.delete('q'); |
| 55 | + } |
| 56 | + window.history.replaceState({}, '', url.toString()); |
| 57 | + }, [query]); |
| 58 | + |
| 59 | + useEffect(() => { |
| 60 | + fuseRef.current = new Fuse(items, { |
| 61 | + keys: ['title', 'description', 'tags'], |
| 62 | + threshold: 0.3, |
| 63 | + includeScore: true, |
| 64 | + ignoreLocation: true, |
| 65 | + }); |
| 66 | + }, [items]); |
| 67 | + |
| 68 | + useEffect(() => { |
| 69 | + if (query.trim() === '') { |
| 70 | + setResults(items); |
| 71 | + return; |
| 72 | + } |
| 73 | + if (fuseRef.current) { |
| 74 | + const searchResults = fuseRef.current.search(query).map(r => r.item); |
| 75 | + setResults(searchResults); |
| 76 | + } |
| 77 | + }, [query, items]); |
| 78 | + |
| 79 | + const posts = results.filter(r => r.type === 'post'); |
| 80 | + const frases = results.filter(r => r.type === 'frase'); |
| 81 | + const activeResults = activeTab === 'posts' ? posts : frases; |
| 82 | + |
| 83 | + const handleSuggestion = (suggestion: string) => { |
| 84 | + setQuery(suggestion); |
| 85 | + }; |
| 86 | + |
| 87 | + return ( |
| 88 | + <div className="w-full"> |
| 89 | + <div className="mb-12 relative"> |
| 90 | + <svg className="absolute left-6 top-1/2 -translate-y-1/2 text-[#525252] w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg> |
| 91 | + <input |
| 92 | + type="text" |
| 93 | + autoFocus |
| 94 | + value={query} |
| 95 | + onChange={e => setQuery(e.target.value)} |
| 96 | + placeholder="Buscar ideas, reflexiones, frases..." |
| 97 | + className="w-full bg-[#111111] border border-[#1f1f1f] focus:border-lime-400 focus:ring-1 focus:ring-lime-400 rounded-xl text-[#f5f5f5] text-xl md:text-2xl px-16 py-6 outline-none transition-all placeholder:text-[#525252]" |
| 98 | + /> |
| 99 | + {query && ( |
| 100 | + <button |
| 101 | + onClick={() => setQuery('')} |
| 102 | + className="absolute right-6 top-1/2 -translate-y-1/2 text-[#525252] hover:text-[#f5f5f5] p-2" |
| 103 | + > |
| 104 | + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> |
| 105 | + </button> |
| 106 | + )} |
| 107 | + </div> |
| 108 | + |
| 109 | + <div className="flex border-b border-[#1f1f1f] mb-8"> |
| 110 | + <button |
| 111 | + onClick={() => setActiveTab('posts')} |
| 112 | + className={`px-6 py-4 font-medium text-sm transition-colors border-b-2 ${activeTab === 'posts' ? 'border-lime-400 text-[#f5f5f5]' : 'border-transparent text-[#525252] hover:text-[#a3a3a3]'}`} |
| 113 | + > |
| 114 | + Posts ({posts.length}) |
| 115 | + </button> |
| 116 | + <button |
| 117 | + onClick={() => setActiveTab('frases')} |
| 118 | + className={`px-6 py-4 font-medium text-sm transition-colors border-b-2 ${activeTab === 'frases' ? 'border-lime-400 text-[#f5f5f5]' : 'border-transparent text-[#525252] hover:text-[#a3a3a3]'}`} |
| 119 | + > |
| 120 | + Frases ({frases.length}) |
| 121 | + </button> |
| 122 | + </div> |
| 123 | + |
| 124 | + <div className="mb-8 text-[#737373] text-sm"> |
| 125 | + {query ? ( |
| 126 | + <span>{activeResults.length} resultados para "{query}"</span> |
| 127 | + ) : ( |
| 128 | + <span>Mostrando todo el contenido ({activeResults.length})</span> |
| 129 | + )} |
| 130 | + </div> |
| 131 | + |
| 132 | + {activeResults.length === 0 ? ( |
| 133 | + <div className="text-center py-20 border border-dashed border-[#1f1f1f] rounded-xl bg-[#0a0a0a]"> |
| 134 | + <p className="text-[#a3a3a3] text-lg mb-4">Ningún resultado para "{query}"</p> |
| 135 | + <p className="text-[#525252] text-sm mb-6">Sugerencias para explorar:</p> |
| 136 | + <div className="flex flex-wrap justify-center gap-3"> |
| 137 | + {['poder', 'biblia', 'silencio', 'envidia', 'reflexiones'].map(suggestion => ( |
| 138 | + <button |
| 139 | + key={suggestion} |
| 140 | + onClick={() => handleSuggestion(suggestion)} |
| 141 | + className="px-4 py-2 rounded-full border border-[#1f1f1f] bg-[#111111] text-[#737373] hover:text-[#a3e635] hover:border-lime-400/30 text-sm transition-colors" |
| 142 | + > |
| 143 | + {suggestion} |
| 144 | + </button> |
| 145 | + ))} |
| 146 | + </div> |
| 147 | + </div> |
| 148 | + ) : ( |
| 149 | + <div className={activeTab === 'frases' ? 'columns-1 md:columns-2 gap-6' : 'flex flex-col gap-6'}> |
| 150 | + {activeResults.map(item => ( |
| 151 | + activeTab === 'posts' ? ( |
| 152 | + <a |
| 153 | + key={item.id} |
| 154 | + href={item.url} |
| 155 | + className="block p-6 rounded-xl border border-[#1f1f1f] bg-[#111111] hover:border-[#333] transition-colors group" |
| 156 | + > |
| 157 | + <div className="flex items-center gap-3 text-xs text-[#525252] uppercase tracking-widest mb-3"> |
| 158 | + <time dateTime={item.date}> |
| 159 | + {format(new Date(item.date), "d MMM yyyy", { locale: es })} |
| 160 | + </time> |
| 161 | + {item.tags && item.tags.length > 0 && ( |
| 162 | + <> |
| 163 | + <span>·</span> |
| 164 | + <span className="text-lime-400/70">{item.tags[0]}</span> |
| 165 | + </> |
| 166 | + )} |
| 167 | + </div> |
| 168 | + <h2 className="text-2xl font-bold text-[#f5f5f5] group-hover:text-lime-400 transition-colors mb-3"> |
| 169 | + <HighlightText text={item.title} query={query} /> |
| 170 | + </h2> |
| 171 | + <p className="text-[#a3a3a3] leading-relaxed line-clamp-2"> |
| 172 | + <HighlightText text={item.description} query={query} /> |
| 173 | + </p> |
| 174 | + </a> |
| 175 | + ) : ( |
| 176 | + <div |
| 177 | + key={item.id} |
| 178 | + className="break-inside-avoid mb-6 p-6 rounded-xl border border-[#1f1f1f] bg-[#111111] hover:border-[#333] transition-colors flex flex-col justify-between" |
| 179 | + > |
| 180 | + <p className="text-lg text-[#f5f5f5] font-medium leading-relaxed italic mb-6"> |
| 181 | + "<HighlightText text={item.description} query={query} />" |
| 182 | + </p> |
| 183 | + <div className="flex items-center justify-between mt-auto"> |
| 184 | + <a href={item.url} className="text-xs px-2.5 py-1 rounded bg-[#1f1f1f] text-[#a3a3a3] hover:bg-lime-400/10 hover:text-lime-400 transition-colors"> |
| 185 | + #{item.tags[0]} |
| 186 | + </a> |
| 187 | + <div className="flex items-center gap-2"> |
| 188 | + <StoryGenerator texto={item.description} /> |
| 189 | + <CopyButton text={item.description} /> |
| 190 | + </div> |
| 191 | + </div> |
| 192 | + </div> |
| 193 | + ) |
| 194 | + ))} |
| 195 | + </div> |
| 196 | + )} |
| 197 | + </div> |
| 198 | + ); |
| 199 | +} |
0 commit comments