Expose search in TopNav and add Agora kind presets to the Kind picker
Add a Search icon button to the TopNav right cluster (visible on all breakpoints) that links to /search, so users no longer have to dig into the mobile drawer or profile menu to reach search. Surface Agora's main content kinds in the Search filter Kind picker: - Add Campaigns (33863) and Pledges (36639) to buildKindOptions(), since they are first-class Agora content but live outside EXTRA_KINDS (which drives the sidebar / feed-settings UI and shouldn't be polluted with kinds that don't have their own toggleable feed/sidebar items). - Group the curated Agora set — Campaigns, Pledges, Communities, Posts, Articles, Events, Polls, Photos, Videos — under an 'Agora content' section at the top of the KindPicker dropdown, with the long-tail list of every other supported kind under an 'All kinds' section below. When the user types in the search box the partition collapses to a flat result list.
This commit is contained in:
@@ -44,7 +44,7 @@ type KindOption = {
|
||||
|
||||
// ─── Kind options (built once) ───────────────────────────────────────────────
|
||||
|
||||
import { buildKindOptions } from '@/lib/feedFilterUtils';
|
||||
import { buildKindOptions, AGORA_PRESET_KIND_VALUES } from '@/lib/feedFilterUtils';
|
||||
|
||||
// ─── useScrollCarets ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -152,6 +152,25 @@ export function KindPicker({ value, options, onChange }: {
|
||||
);
|
||||
}, [options, search]);
|
||||
|
||||
// Partition into Agora preset (top of picker) and the rest.
|
||||
// When the user is searching, we skip the partition and show a flat list.
|
||||
const presetSet = useMemo(() => new Set(AGORA_PRESET_KIND_VALUES), []);
|
||||
const { presetOptions, otherOptions } = useMemo(() => {
|
||||
if (search) return { presetOptions: [], otherOptions: filtered };
|
||||
const preset: KindOption[] = [];
|
||||
const other: KindOption[] = [];
|
||||
// Preserve AGORA_PRESET_KIND_VALUES order for the preset section.
|
||||
const byValue = new Map(filtered.map((o) => [o.value, o]));
|
||||
for (const v of AGORA_PRESET_KIND_VALUES) {
|
||||
const opt = byValue.get(v);
|
||||
if (opt) preset.push(opt);
|
||||
}
|
||||
for (const o of filtered) {
|
||||
if (!presetSet.has(o.value)) other.push(o);
|
||||
}
|
||||
return { presetOptions: preset, otherOptions: other };
|
||||
}, [filtered, presetSet, search]);
|
||||
|
||||
const selected = value === 'all' || value === 'custom' ? null : options.find((o) => o.value === value);
|
||||
const SelectedIcon = selected?.icon;
|
||||
|
||||
@@ -199,7 +218,22 @@ export function KindPicker({ value, options, onChange }: {
|
||||
{canScrollUp && <KindScrollCaret direction="up" onMouseEnter={() => startScroll('up')} onMouseLeave={stopScroll} />}
|
||||
<div ref={refCallback} className="overflow-y-auto flex-1 min-h-0" onScroll={onScroll}>
|
||||
{!search && <KindPickerItem icon={null} label="All kinds" active={value === 'all'} onClick={() => handleSelect('all')} />}
|
||||
{filtered.map((opt) => (
|
||||
{!search && presetOptions.length > 0 && (
|
||||
<>
|
||||
<div className="px-2.5 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Agora content
|
||||
</div>
|
||||
{presetOptions.map((opt) => (
|
||||
<KindPickerItem key={opt.value} icon={opt.icon ?? null} label={opt.label} active={value === opt.value} onClick={() => handleSelect(opt.value)} />
|
||||
))}
|
||||
{otherOptions.length > 0 && (
|
||||
<div className="px-2.5 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
All kinds
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{otherOptions.map((opt) => (
|
||||
<KindPickerItem key={opt.value} icon={opt.icon ?? null} label={opt.label} active={value === opt.value} onClick={() => handleSelect(opt.value)} />
|
||||
))}
|
||||
{(!search || 'custom'.includes(search.toLowerCase())) && (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, type ComponentType } from 'react';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { Link, NavLink, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Activity,
|
||||
Bell,
|
||||
@@ -54,6 +54,9 @@ export function TopNav() {
|
||||
const { user } = useCurrentUser();
|
||||
const { orderedItems } = useFeedSettings();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const goToSearch = () => navigate('/search');
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-40 w-full border-b border-border bg-background/85 backdrop-blur supports-[backdrop-filter]:bg-background/70">
|
||||
@@ -89,6 +92,18 @@ export function TopNav() {
|
||||
|
||||
{/* Right cluster */}
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
{/* Search — navigates to the /search page. Visible on all
|
||||
breakpoints so users can always reach search from the chrome. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={goToSearch}
|
||||
className="shrink-0 size-9 rounded-full flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-secondary motion-safe:transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
aria-label="Search"
|
||||
title="Search"
|
||||
>
|
||||
<Search className="size-5" />
|
||||
</button>
|
||||
|
||||
{/* LoginArea handles both logged-in (account avatar dropdown) and
|
||||
logged-out (Log in / Sign up) states. We render it inline-flex
|
||||
and let it style its own children. */}
|
||||
|
||||
@@ -10,9 +10,54 @@ type KindOption = {
|
||||
icon: React.ComponentType<{ className?: string }> | undefined;
|
||||
};
|
||||
|
||||
/** Build the kind options from EXTRA_KINDS definitions. */
|
||||
/**
|
||||
* Agora-native kinds that are not modeled in EXTRA_KINDS (which drives the
|
||||
* sidebar / feed-settings UI). We surface them in the search Kind picker
|
||||
* because they are first-class searchable content on Agora.
|
||||
*/
|
||||
const AGORA_NATIVE_KIND_OPTIONS: KindOption[] = [
|
||||
{
|
||||
value: '33863',
|
||||
label: 'Campaigns (33863)',
|
||||
description: 'Fundraising campaigns',
|
||||
parentId: 'campaigns',
|
||||
icon: CONTENT_KIND_ICONS['campaigns'],
|
||||
},
|
||||
{
|
||||
value: '36639',
|
||||
label: 'Pledges (36639)',
|
||||
description: 'Donor pledges for concrete actions',
|
||||
parentId: 'actions',
|
||||
icon: CONTENT_KIND_ICONS['actions'],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Agora's curated "main content" kinds, surfaced as a preset section at the
|
||||
* top of the KindPicker. Order is the order they appear in the picker.
|
||||
*
|
||||
* Includes Agora-native kinds plus the existing-NIP kinds Agora foregrounds
|
||||
* (campaigns, pledges, communities, posts, articles, events, polls, photos,
|
||||
* videos). Excludes social-signal kinds (reactions, reposts, zaps) and
|
||||
* stats snapshots, which users rarely filter on directly.
|
||||
*/
|
||||
export const AGORA_PRESET_KIND_VALUES: readonly string[] = [
|
||||
'33863', // Campaigns
|
||||
'36639', // Pledges
|
||||
'34550', // Communities
|
||||
'1', // Posts
|
||||
'30023', // Articles
|
||||
'31923', // Events (time)
|
||||
'1068', // Polls
|
||||
'20', // Photos
|
||||
'21', // Videos
|
||||
] as const;
|
||||
|
||||
/** Build the kind options from EXTRA_KINDS definitions plus Agora-native kinds. */
|
||||
export function buildKindOptions(): KindOption[] {
|
||||
const options: KindOption[] = [];
|
||||
// Agora-native kinds appear first so they're easy to find.
|
||||
for (const opt of AGORA_NATIVE_KIND_OPTIONS) options.push(opt);
|
||||
for (const def of EXTRA_KINDS) {
|
||||
if (def.subKinds) {
|
||||
for (const sub of def.subKinds) {
|
||||
|
||||
Reference in New Issue
Block a user