import { PostCard } from "$/components/feeds/PostCard"; import { usePostNavigation } from "$/components/posts/hooks/usePostNavigation"; import { Icon } from "$/components/shared/Icon"; import { SearchController } from "$/lib/api/search"; import type { NetworkSearchResult } from "$/lib/api/types/search"; import { buildHashtagQuery, buildPostSearchRoute, decodeHashtagRouteTag, formatHashtagLabel, parsePostSearchFilters, toLocalDayStartIso, toLocalDayUntilIso, } from "$/lib/search-routes"; import type { PostSearchFilters } from "$/lib/search-routes"; import { normalizeError } from "$/lib/utils/text"; import { useLocation, useNavigate, useParams } from "@solidjs/router"; import * as logger from "@tauri-apps/plugin-log"; import { createEffect, createMemo, For, Match, Show, Switch } from "solid-js"; import { createStore } from "solid-js/store"; import { Motion, Presence } from "solid-motionone"; import { PostSearchFiltersRow } from "./PostSearchFilters"; import { SearchEmptyState } from "./SearchEmptyState"; import type { EmptyStateReason } from "./types"; type HashtagPanelState = { error: string | null; hasSearched: boolean; loading: boolean; results: NetworkSearchResult | null; }; export function HashtagPanel() { const location = useLocation(); const navigate = useNavigate(); const params = useParams<{ hashtag: string }>(); const postNavigation = usePostNavigation(); const [state, setState] = createStore({ error: null, hasSearched: false, loading: false, results: null, }); let debounceTimer: ReturnType | undefined; const tag = createMemo(() => decodeHashtagRouteTag(params.hashtag)); const filters = createMemo(() => parsePostSearchFilters(location.search)); const hashtagLabel = createMemo(() => formatHashtagLabel(tag() ?? "")); function replaceRoute(next: Partial) { const currentTag = tag(); if (!currentTag) { return; } void navigate(buildPostSearchRoute(location.pathname, location.search, { ...filters(), ...next })); } async function performSearch(f: PostSearchFilters, t: string) { try { const results = await SearchController.searchPostsNetwork({ author: f.author || null, limit: 25, mentions: f.mentions || null, query: buildHashtagQuery(t), since: f.since ? toLocalDayStartIso(f.since) : null, sort: f.sort, tags: f.tags, until: f.until ? toLocalDayUntilIso(f.until) : null, }); setState({ error: null, hasSearched: true, loading: false, results }); } catch (error) { const errorMessage = normalizeError(error); logger.error("hashtag search failed", { keyValues: { error: errorMessage, hashtag: t, sort: f.sort } }); setState({ error: errorMessage, hasSearched: true, loading: false, results: null }); } } createEffect(() => { const currentTag = tag(); const activeFilters = filters(); clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { if (!currentTag) { setState({ error: "This hashtag could not be opened.", hasSearched: false, loading: false, results: null }); return; } setState((previous) => ({ ...previous, error: null, loading: true })); void performSearch(activeFilters, currentTag); }, 300); }); return (
replaceRoute(next)} />
}>
{() =>
}
); } function hasAdvancedNetworkFilters(filters: PostSearchFilters) { return !!(filters.author || filters.mentions || filters.since || filters.until || filters.tags.length > 0); } function HashtagState(props: HashtagPanelState & { onOpenThread: (uri: string) => void }) { return ( {(results) => (
{(post, index) => ( props.onOpenThread(uri)} /> )}
)}
); } function EmptyState(props: { reason: EmptyStateReason }) { return ( ); } function HashtagHero(props: { hashtagLabel: string }) { return (
Hashtag

{props.hashtagLabel}

Search Bluesky for this hashtag.

); }