feat: search bar in topbar with Gmail-style query syntax
- Search form in topbar routes to /app/search?q=... - SearchResults page with infinite scroll, same row style as FolderView - Supports Gmail-like operators: from:, is:unread, has:attachment, after:, before: - SearchApi.query() calls GET /search which runs through the existing GmailQueryParser - Input sanitised via encodeURIComponent on submit; external links use noopener/noreferrer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Link, Outlet, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { AuthApi, SyncApi, AnalyticsApi } from '../api/client.js';
|
||||
import Logo from './Logo.jsx';
|
||||
@@ -102,6 +102,8 @@ function FolderLink({ item, active, count, collapsed, extra }) {
|
||||
|
||||
export default function Layout() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [searchQuery, setSearchQuery] = useState(() => searchParams.get('q') ?? '');
|
||||
const [sidebarOpen, setSidebarOpen] = useState(() => {
|
||||
try { return localStorage.getItem(LS_OPEN) !== 'false'; } catch { return true; }
|
||||
});
|
||||
@@ -158,6 +160,12 @@ export default function Layout() {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const submitSearch = (e) => {
|
||||
e.preventDefault();
|
||||
const q = searchQuery.trim();
|
||||
if (q) navigate(`/app/search?q=${encodeURIComponent(q)}`);
|
||||
};
|
||||
|
||||
const startSync = async () => {
|
||||
try { await SyncApi.incremental(); } catch { /* ignore */ }
|
||||
if (loc.pathname !== '/app') navigate('/app');
|
||||
@@ -199,6 +207,17 @@ export default function Layout() {
|
||||
<Link key={n.to} to={n.to} className={isNavActive(n) ? 'active' : ''}>{n.label}</Link>
|
||||
))}
|
||||
</nav>
|
||||
<form className="search-form" onSubmit={submitSearch}>
|
||||
<input
|
||||
className="search-input"
|
||||
type="search"
|
||||
placeholder="Search… (from:, is:unread, has:attachment)"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
aria-label="Search emails"
|
||||
/>
|
||||
<button type="submit" className="search-btn" aria-label="Search">🔍</button>
|
||||
</form>
|
||||
<div className="spacer" />
|
||||
<button onClick={startSync}>Sync now</button>
|
||||
<span className="user">{user?.email}</span>
|
||||
|
||||
Reference in New Issue
Block a user