search page ui fixes
This commit is contained in:
+62
-122
@@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buildSearchResultUrl } from "@/lib/text-highlight";
|
||||
import { KnowledgebaseIndex } from "@/components/knowledgebase-index";
|
||||
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
||||
import { SearchFiltersMenu } from "./filters-menu";
|
||||
import {
|
||||
Search,
|
||||
Loader2,
|
||||
@@ -197,6 +199,9 @@ function SearchInner() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// Drives the popover-vs-drawer split inside SearchFiltersMenu —
|
||||
// same convention as the rest of the app's overflow menus.
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
|
||||
// Read initial state from URL
|
||||
const initialQuery = searchParams.get("q") || "";
|
||||
@@ -241,12 +246,8 @@ function SearchInner() {
|
||||
|
||||
// When the query is empty, derive these as "no results" during render
|
||||
// instead of setting state inside an effect.
|
||||
const effectiveData =
|
||||
isQueryEmpty || isUserSearch ? null : data;
|
||||
const hasSearched =
|
||||
isUserSearch
|
||||
? !isUserQueryEmpty
|
||||
: !isQueryEmpty;
|
||||
const effectiveData = isQueryEmpty || isUserSearch ? null : data;
|
||||
const hasSearched = isUserSearch ? !isUserQueryEmpty : !isQueryEmpty;
|
||||
|
||||
// Sync URL when state changes
|
||||
const pushUrl = useCallback(
|
||||
@@ -423,11 +424,6 @@ function SearchInner() {
|
||||
<h1 className="mt-4 font-[family-name:var(--font-title)] font-semibold text-2xl sm:text-4xl leading-tight text-foreground">
|
||||
Find a word, a verse, a name.
|
||||
</h1>
|
||||
<p className="mt-3 max-w-xl text-base text-muted-foreground leading-relaxed">
|
||||
Smart search across books, letters, lectures, essays,
|
||||
journals and magazines. Diacritics are ignored, so
|
||||
“Krsna” finds “Kṛṣṇa.”
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-4 sm:px-6 pb-16">
|
||||
@@ -473,74 +469,20 @@ function SearchInner() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content Type Tabs */}
|
||||
{/* Filters — single trigger that opens a popover (desktop) or
|
||||
drawer (mobile) of multi-select checkboxes. The trigger
|
||||
carries a count badge when one or more filters are active.
|
||||
Users is mutually exclusive with content narrowing inside
|
||||
the menu. */}
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{/* Users chip — toggles the "@" prefix on the input. Sits at
|
||||
the start of the row so its at-sign icon reads as a kind
|
||||
of "search mode" indicator. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleUsersChip}
|
||||
aria-pressed={isUserChipActive}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
|
||||
isUserChipActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<AtSign className="size-4" />
|
||||
Users
|
||||
{userData &&
|
||||
isUserChipActive &&
|
||||
userData.results.length > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-1 inline-flex items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-semibold leading-none",
|
||||
"bg-primary-foreground/20 text-primary-foreground",
|
||||
)}
|
||||
>
|
||||
{userData.results.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{CONTENT_TABS.map((tab) => {
|
||||
const active = !isUserChipActive && isTypeActive(tab.value);
|
||||
return (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => handleTypeChange(tab.value)}
|
||||
disabled={isUserChipActive}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
|
||||
active
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground",
|
||||
// When the @ prefix is on, content tabs don't apply —
|
||||
// grey them out so the chip group reads as "user
|
||||
// mode is exclusive".
|
||||
isUserChipActive &&
|
||||
"opacity-40 cursor-not-allowed hover:bg-muted hover:text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
{effectiveData &&
|
||||
active &&
|
||||
selectedTypes.size <= 1 &&
|
||||
effectiveData.total > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-1 inline-flex items-center justify-center rounded-full px-1.5 py-0.5 text-xs font-semibold leading-none",
|
||||
"bg-primary-foreground/20 text-primary-foreground",
|
||||
)}
|
||||
>
|
||||
{effectiveData.total.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<SearchFiltersMenu
|
||||
contentTabs={CONTENT_TABS}
|
||||
selectedTypes={selectedTypes}
|
||||
onTypeChange={handleTypeChange}
|
||||
isUserChipActive={isUserChipActive}
|
||||
onToggleUsers={toggleUsersChip}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Results Area */}
|
||||
@@ -633,48 +575,48 @@ function SearchInner() {
|
||||
!isUserSearch &&
|
||||
effectiveData &&
|
||||
effectiveData.results.length > 0 && (
|
||||
<>
|
||||
{/* Results count + top pagination */}
|
||||
<div className="mb-4 flex items-center justify-between flex-wrap gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{effectiveData.total.toLocaleString()}{" "}
|
||||
{effectiveData.total === 1 ? "result " : "results "} for
|
||||
“
|
||||
{effectiveData.query}”
|
||||
</p>
|
||||
{effectiveData.total_pages > 1 && (
|
||||
<PaginationControls
|
||||
page={effectiveData.page}
|
||||
totalPages={effectiveData.total_pages}
|
||||
onPageChange={handlePageChange}
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Result list */}
|
||||
<div className="space-y-3">
|
||||
{effectiveData.results.map((result, i) => (
|
||||
<ResultCard
|
||||
key={`${result.collection}-${result.id || i}`}
|
||||
result={result}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom pagination */}
|
||||
{effectiveData.total_pages > 1 && (
|
||||
<div className="mt-8">
|
||||
<PaginationControls
|
||||
page={effectiveData.page}
|
||||
totalPages={effectiveData.total_pages}
|
||||
total={effectiveData.total}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
<>
|
||||
{/* Results count + top pagination */}
|
||||
<div className="mb-4 flex items-center justify-between flex-wrap gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{effectiveData.total.toLocaleString()}{" "}
|
||||
{effectiveData.total === 1 ? "result " : "results "} for
|
||||
“
|
||||
{effectiveData.query}”
|
||||
</p>
|
||||
{effectiveData.total_pages > 1 && (
|
||||
<PaginationControls
|
||||
page={effectiveData.page}
|
||||
totalPages={effectiveData.total_pages}
|
||||
onPageChange={handlePageChange}
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Result list */}
|
||||
<div className="space-y-3">
|
||||
{effectiveData.results.map((result, i) => (
|
||||
<ResultCard
|
||||
key={`${result.collection}-${result.id || i}`}
|
||||
result={result}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom pagination */}
|
||||
{effectiveData.total_pages > 1 && (
|
||||
<div className="mt-8">
|
||||
<PaginationControls
|
||||
page={effectiveData.page}
|
||||
totalPages={effectiveData.total_pages}
|
||||
total={effectiveData.total}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -902,9 +844,7 @@ function UserResultCard({ user }: { user: UserResult }) {
|
||||
<p className="text-sm font-semibold text-foreground truncate">
|
||||
{user.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
@{user.handle}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">@{user.handle}</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user