From 8b4a639c77d5e0b9f6dd28c6b53ad948d6a8b3a1 Mon Sep 17 00:00:00 2001 From: matteoscrugli Date: Mon, 22 Dec 2025 19:09:31 +0100 Subject: [PATCH] Add search page with popup UI --- frontend/src/App.tsx | 2 + frontend/src/locales/en.json | 6 ++ frontend/src/locales/it.json | 6 ++ frontend/src/pages/Search.tsx | 140 +++++++++++++++++++++++++++++++++ frontend/src/styles/Search.css | 130 ++++++++++++++++++++++++++++++ 5 files changed, 284 insertions(+) create mode 100644 frontend/src/pages/Search.tsx create mode 100644 frontend/src/styles/Search.css diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 30e9d22..620acb2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ import Dashboard from './pages/Dashboard'; import Feature1 from './pages/Feature1'; import Feature2 from './pages/Feature2'; import Feature3 from './pages/Feature3'; +import Search from './pages/Search'; import Notifications from './pages/Notifications'; import APIKeys from './pages/APIKeys'; import AdminPanel from './pages/AdminPanel'; @@ -70,6 +71,7 @@ function AppRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 8553c98..0405e1f 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -59,6 +59,12 @@ "feature3": "Feature 3 Integration", "comingSoon": "Coming Soon" }, + "searchPage": { + "title": "Search", + "placeholder": "Search the system", + "hint": "Start typing to search", + "noResults": "No results" + }, "featuresPage": { "title": "Features", "subtitle": "Configure application features", diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index b8c6644..90a1ae7 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -59,6 +59,12 @@ "feature3": "Integrazione Funzione 3", "comingSoon": "Prossimamente" }, + "searchPage": { + "title": "Cerca", + "placeholder": "Cerca nel sistema", + "hint": "Inizia a digitare per cercare", + "noResults": "Nessun risultato" + }, "featuresPage": { "title": "Funzionalità", "subtitle": "Configura le funzionalità dell'applicazione", diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx new file mode 100644 index 0000000..aea7d53 --- /dev/null +++ b/frontend/src/pages/Search.tsx @@ -0,0 +1,140 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from '../contexts/LanguageContext'; +import { useSidebar } from '../contexts/SidebarContext'; +import '../styles/Search.css'; + +type SearchResult = { + id: string; + title: string; + subtitle?: string; + icon?: string; +}; + +const PANEL_ESTIMATED_HEIGHT = 260; + +export default function Search() { + const { t } = useTranslation(); + const { toggleMobileMenu } = useSidebar(); + const [query, setQuery] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const [direction, setDirection] = useState<'up' | 'down'>('down'); + const wrapperRef = useRef(null); + const inputRef = useRef(null); + + const results = useMemo(() => { + const term = query.trim().toLowerCase(); + if (!term) return []; + return []; + }, [query]); + + useEffect(() => { + if (!isOpen) return; + const updateDirection = () => { + const rect = wrapperRef.current?.getBoundingClientRect(); + if (!rect || typeof window === 'undefined') return; + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const shouldOpenUp = spaceBelow < PANEL_ESTIMATED_HEIGHT && spaceAbove > spaceBelow; + setDirection(shouldOpenUp ? 'up' : 'down'); + }; + updateDirection(); + window.addEventListener('resize', updateDirection); + return () => window.removeEventListener('resize', updateDirection); + }, [isOpen]); + + const handleFocus = () => { + setIsOpen(true); + }; + + const handleBlur = (event: React.FocusEvent) => { + const nextTarget = event.relatedTarget as Node | null; + if (nextTarget && wrapperRef.current?.contains(nextTarget)) { + return; + } + setIsOpen(false); + }; + + const handleChange = (event: React.ChangeEvent) => { + setQuery(event.target.value); + if (!isOpen) { + setIsOpen(true); + } + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setIsOpen(false); + inputRef.current?.blur(); + } + }; + + const showResults = query.trim().length > 0; + const showPopover = isOpen; + const hintText = showResults + ? t.searchPage.noResults + : t.searchPage.hint; + + return ( +
+
+
+ +
+ {t.searchPage.title} +
+
+
+ +
+
+
+
+ search + +
+ +
+ {results.length > 0 ? ( +
+ {results.map((result) => ( + + ))} +
+ ) : ( +
+ manage_search + {hintText} +
+ )} +
+
+
+
+
+ ); +} diff --git a/frontend/src/styles/Search.css b/frontend/src/styles/Search.css new file mode 100644 index 0000000..a5cee4f --- /dev/null +++ b/frontend/src/styles/Search.css @@ -0,0 +1,130 @@ +.search-panel { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.search-input-wrapper { + position: relative; + width: 100%; +} + +.search-input-group { + display: flex; + align-items: center; + gap: var(--element-gap); + padding: var(--input-padding); + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + width: 100%; +} + +.search-input-group:focus-within { + border-color: rgba(var(--color-accent-rgb), 0.35); + box-shadow: var(--shadow-ring); +} + +.search-input-group .material-symbols-outlined { + color: var(--color-text-secondary); + font-size: 20px; +} + +.search-input-group input { + border: none; + outline: none; + background: transparent; + color: var(--color-text-primary); + width: 100%; +} + +.search-popover { + position: absolute; + left: 0; + right: 0; + top: calc(100% + 0.5rem); + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + opacity: 0; + transform: translateY(-4px); + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease; + z-index: 20; +} + +.search-popover[data-direction='up'] { + top: auto; + bottom: calc(100% + 0.5rem); + transform: translateY(4px); +} + +.search-popover.open { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +.search-results { + display: flex; + flex-direction: column; + max-height: 260px; + overflow-y: auto; +} + +.search-result { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + border: none; + background: transparent; + color: var(--color-text-primary); + text-align: left; + cursor: pointer; + border-bottom: 1px solid var(--color-card-outline); +} + +.search-result:last-child { + border-bottom: none; +} + +.search-result:hover { + background: var(--color-bg-elevated); +} + +.search-result-icon { + color: var(--color-text-secondary); + font-size: 20px; +} + +.search-result-text { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.search-result-title { + font-weight: 600; +} + +.search-result-subtitle { + font-size: 0.85rem; + color: var(--color-text-secondary); +} + +.search-empty { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem; + color: var(--color-text-secondary); + font-size: 0.95rem; +} + +.search-empty .material-symbols-outlined { + font-size: 20px; + color: var(--color-text-muted); +}