/
Leks76
/
Next-tutorial
Обзор
Документация
Войти
/
Leks76
/
Next-tutorial
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/components/TextSearch.jsx
173 строки
6 KB
Leks76
next tutorial complit
27 апр 2026, 07:21
Верифицирован
27 апр 2026, 07:21
2552a7e
Код
Авторство
О чём код?
"use client"; import React, { useState, useMemo, useRef, useEffect } from "react"; import { Input } from "./ui/input"; import { Button2 } from "./ui/Button2"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "./ui/card"; import { Badge } from "./ui/badge"; import { Search, ArrowUp, ArrowDown, X } from "lucide-react"; // Mock data: Sample text to search through const SAMPLE_TEXT = ` React (также известный как React.js или ReactJS) — это JavaScript-библиотека с открытым исходным кодом для разработки пользовательских интерфейсов. React поддерживает разработку одностраничных и мобильных приложений. Библиотека React была создана Джорданом Уолком, сотрудником Facebook. В настоящее время React поддерживается и развивается компанией Meta (бывший Facebook) и сообществом разработчиков. Основные особенности React включают использование виртуального DOM, односторонний поток данных и компонентный подход. Разработчики используют React для создания быстрых и интерактивных веб-приложений. `; const TextSearch = () => { const [searchQuery, setSearchQuery] = useState(""); const [activeMatchIndex, setActiveMatchIndex] = useState(0); const containerRef = typeof window !== 'undefined' ? useRef <HTMLDivElement> (null) : useRef(null); // Calculate matches based on search query const matches = useMemo(() => { if (!searchQuery.trim()) return []; const regex = new RegExp(`(${searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); const found = SAMPLE_TEXT.match(regex); return found || []; }, [searchQuery]); // Reset active index when matches change or query is cleared useEffect(() => { if (searchQuery.trim() === "") { setActiveMatchIndex(0); } }, [searchQuery, matches.length]); const matchRef = useRef(null); const highlightText = (text, query) => { if (!query.trim()) return text; try { const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); const parts = text.split(regex); return parts.map((part, index) => { if (regex.test(part)) { // Determine if this is the active match const isCurrentMatch = index === (activeMatchIndex * 2) + 1; return ( <mark key={index} ref={isCurrentMatch ? matchRef : null} className={`rounded px-0.5 transition-colors ${isCurrentMatch ? "bg-yellow-400 text-black dark:bg-yellow-600 dark:text-white" : "bg-yellow-200 text-black dark:bg-yellow-800/50 dark:text-white" }`} > {part} </mark> ); } return part; }); } catch (e) { return text; } }; // Scroll to active match useEffect(() => { if (matchRef.current && containerRef.current) { matchRef.current.scrollIntoView({ behavior: "smooth", block: "center" }); } }, [activeMatchIndex]); const handleNext = () => { if (matches.length === 0) return; setActiveMatchIndex((prev) => (prev + 1) % matches.length); }; const handlePrev = () => { if (matches.length === 0) return; setActiveMatchIndex((prev) => (prev - 1 + matches.length) % matches.length); }; const handleClear = () => { setSearchQuery(""); setActiveMatchIndex(0); }; return ( <div className="container py-10 flex flex-col items-center justify-center min-h-[calc(100vh-4rem)]"> <Card className="w-full max-w-2xl shadow-lg"> <CardHeader> <CardTitle className="flex items-center gap-2"> <Search className="w-5 h-5" /> Поиск в тексте </CardTitle> <CardDescription> Введите слово для поиска по тексту ниже. Используйте стрелки для навигации. </CardDescription> </CardHeader> <CardContent className="space-y-4"> {/* Search Controls */} <div className="flex gap-2"> <div className="relative flex-1"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" /> <Input type="text" placeholder="Введите запрос..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pl-9 pr-10" /> {searchQuery && ( <button onClick={handleClear} className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground" > <X className="h-4 w-4" /> </button> )} </div> {matches.length > 0 && ( <div className="flex items-center gap-1"> <Button2 variant="outline" size="icon" onClick={handlePrev} title="Предыдущее"> <ArrowUp className="h-4 w-4" /> </Button2> <Button2 variant="outline" size="icon" onClick={handleNext} title="Следующее"> <ArrowDown className="h-4 w-4" /> </Button2> </div> )} </div> {/* Match Status */} {searchQuery && ( <div className="flex items-center gap-2 text-sm text-muted-foreground"> {matches.length > 0 ? ( <Badge variant="secondary"> {activeMatchIndex + 1} из {matches.length} </Badge> ) : ( <span className="text-destructive">Совпадений не найдено</span> )} </div> )} {/* Text Content Area */} <div // ref={containerRef} className="p-4 rounded-md border bg-muted/30 h-64 overflow-y-auto text-pretty leading-relaxed text-sm" > {highlightText(SAMPLE_TEXT.trim(), searchQuery)} </div> </CardContent> </Card> </div> ); } export default TextSearch;