diff --git a/src/webapp/features/search/components/profileCard/ProfileCard.tsx b/src/webapp/features/search/components/profileCard/ProfileCard.tsx new file mode 100644 index 00000000..770ac27b --- /dev/null +++ b/src/webapp/features/search/components/profileCard/ProfileCard.tsx @@ -0,0 +1,43 @@ +import { ProfileView } from '@/api-client'; +import { Avatar, Card, Group, Stack, Text } from '@mantine/core'; +import Link from 'next/link'; + +interface Props { + profile: ProfileView; +} + +export default function ProfileCard(props: Props) { + return ( + + + + + + + + {props.profile.displayName || props.profile.handle} + + + @{props.profile.handle} + + {props.profile.description && ( + + {props.profile.description} + + )} + + + + + ); +} diff --git a/src/webapp/features/search/components/searchEmptyResults/SearchEmptyResults.tsx b/src/webapp/features/search/components/searchEmptyResults/SearchEmptyResults.tsx index 622c85cb..789e61a4 100644 --- a/src/webapp/features/search/components/searchEmptyResults/SearchEmptyResults.tsx +++ b/src/webapp/features/search/components/searchEmptyResults/SearchEmptyResults.tsx @@ -3,9 +3,12 @@ import { BiSearch } from 'react-icons/bi'; interface Props { query: string; + type?: 'cards' | 'profiles'; } export default function SearchEmptyResults(props: Props) { + const type = props.type || 'results'; + return (
@@ -14,7 +17,7 @@ export default function SearchEmptyResults(props: Props) { No results found - No cards found for "{props.query}". Try a different search term. + No {type} found for "{props.query}". Try a different search term.
diff --git a/src/webapp/features/search/containers/profileSearchResultsContainer/Error.ProfileSearchResultsContainer.tsx b/src/webapp/features/search/containers/profileSearchResultsContainer/Error.ProfileSearchResultsContainer.tsx new file mode 100644 index 00000000..0ad93494 --- /dev/null +++ b/src/webapp/features/search/containers/profileSearchResultsContainer/Error.ProfileSearchResultsContainer.tsx @@ -0,0 +1,5 @@ +import { Alert } from '@mantine/core'; + +export default function ProfileSearchResultsContainerError() { + return ; +} diff --git a/src/webapp/features/search/containers/profileSearchResultsContainer/ProfileSearchResultsContainer.tsx b/src/webapp/features/search/containers/profileSearchResultsContainer/ProfileSearchResultsContainer.tsx new file mode 100644 index 00000000..c3be0ac1 --- /dev/null +++ b/src/webapp/features/search/containers/profileSearchResultsContainer/ProfileSearchResultsContainer.tsx @@ -0,0 +1,51 @@ +'use client'; + +import useProfileSearch from '../../lib/queries/useProfileSearch'; +import InfiniteScroll from '@/components/contentDisplay/infiniteScroll/InfiniteScroll'; +import { Grid } from '@mantine/core'; +import ProfileSearchResultsContainerError from './Error.ProfileSearchResultsContainer'; +import ProfileCard from '../../components/profileCard/ProfileCard'; +import SearchEmptyResults from '../../components/searchEmptyResults/SearchEmptyResults'; + +interface Props { + query: string; +} + +export default function ProfileSearchResultsContainer(props: Props) { + const { + data, + error, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isPending, + } = useProfileSearch({ query: props.query }); + + const allProfiles = data?.pages.flatMap((page) => page.actors ?? []) ?? []; + + if (error) { + return ; + } + + if (!isPending && allProfiles.length === 0) { + return ; + } + + return ( + + + {allProfiles.map((profile) => ( + + + + ))} + + + ); +} diff --git a/src/webapp/features/search/containers/searchContainer/SearchContainer.tsx b/src/webapp/features/search/containers/searchContainer/SearchContainer.tsx index bc672739..484f57a4 100644 --- a/src/webapp/features/search/containers/searchContainer/SearchContainer.tsx +++ b/src/webapp/features/search/containers/searchContainer/SearchContainer.tsx @@ -1,15 +1,26 @@ 'use client'; import { useState } from 'react'; -import { Container, Stack, TextInput, Button, Group } from '@mantine/core'; +import { + Container, + Stack, + TextInput, + Button, + Group, + SegmentedControl, +} from '@mantine/core'; import { BiSearch } from 'react-icons/bi'; import SearchResultsContainer from '../searchResultsContainer/SearchResultsContainer'; +import ProfileSearchResultsContainer from '../profileSearchResultsContainer/ProfileSearchResultsContainer'; import UserFilterCombobox from '../../components/userFilterCombobox/UserFilterCombobox'; import type { ProfileViewBasic } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; +type SearchType = 'cards' | 'profiles'; + export default function SearchContainer() { const [query, setQuery] = useState(''); const [searchQuery, setSearchQuery] = useState(''); + const [searchType, setSearchType] = useState('cards'); const [selectedUser, setSelectedUser] = useState( null, ); @@ -20,7 +31,7 @@ export default function SearchContainer() { const handleSearch = () => { if (query.trim()) { setSearchQuery(query.trim()); - setSearchUserId(selectedUser?.did); + setSearchUserId(searchType === 'cards' ? selectedUser?.did : undefined); } }; @@ -30,30 +41,58 @@ export default function SearchContainer() { } }; + const handleSearchTypeChange = (value: string) => { + setSearchType(value as SearchType); + // Clear user filter when switching to profile search + if (value === 'profiles') { + setSelectedUser(null); + setSearchUserId(undefined); + } + }; + return ( + + setQuery(event.currentTarget.value)} onKeyPress={handleKeyPress} leftSection={} /> - + {searchType === 'cards' && ( + + )} - {searchQuery && ( + {searchQuery && searchType === 'cards' && ( )} + + {searchQuery && searchType === 'profiles' && ( + + )} ); diff --git a/src/webapp/features/search/containers/searchResultsContainer/SearchResultsContainer.tsx b/src/webapp/features/search/containers/searchResultsContainer/SearchResultsContainer.tsx index 1a142ee6..995a2f97 100644 --- a/src/webapp/features/search/containers/searchResultsContainer/SearchResultsContainer.tsx +++ b/src/webapp/features/search/containers/searchResultsContainer/SearchResultsContainer.tsx @@ -29,7 +29,7 @@ export default function SearchResultsContainer(props: Props) { } if (!isPending && allUrls.length === 0) { - return ; + return ; } return ( diff --git a/src/webapp/features/search/lib/dal.ts b/src/webapp/features/search/lib/dal.ts index f237b9ec..3dd624cf 100644 --- a/src/webapp/features/search/lib/dal.ts +++ b/src/webapp/features/search/lib/dal.ts @@ -12,6 +12,10 @@ interface SemanticSearchParams extends PageParams { userId?: string; } +interface ProfileSearchParams extends PageParams { + cursor?: string; +} + export const semanticSearchUrls = cache( async (query: string, params?: SemanticSearchParams) => { const client = createSembleClient(); @@ -27,3 +31,16 @@ export const semanticSearchUrls = cache( return response; }, ); + +export const searchAtProtoAccounts = cache( + async (query: string, params?: ProfileSearchParams) => { + const client = createSembleClient(); + const response = await client.searchAtProtoAccounts({ + q: query, + limit: params?.limit, + cursor: params?.cursor, + }); + + return response; + }, +); diff --git a/src/webapp/features/search/lib/queries/useProfileSearch.tsx b/src/webapp/features/search/lib/queries/useProfileSearch.tsx new file mode 100644 index 00000000..3e5444d4 --- /dev/null +++ b/src/webapp/features/search/lib/queries/useProfileSearch.tsx @@ -0,0 +1,28 @@ +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { searchAtProtoAccounts } from '../dal'; +import { searchKeys } from '../searchKeys'; + +interface Props { + query: string; + limit?: number; +} + +export default function useProfileSearch(props: Props) { + const limit = props?.limit ?? 16; + + const searchResults = useSuspenseInfiniteQuery({ + queryKey: searchKeys.profileSearchInfinite(props.query, props.limit), + initialPageParam: undefined as string | undefined, + queryFn: ({ pageParam }) => { + return searchAtProtoAccounts(props.query, { + limit, + cursor: pageParam, + }); + }, + getNextPageParam: (lastPage) => { + return lastPage.cursor; + }, + }); + + return searchResults; +} diff --git a/src/webapp/features/search/lib/searchKeys.ts b/src/webapp/features/search/lib/searchKeys.ts index efb97c0d..c1b71667 100644 --- a/src/webapp/features/search/lib/searchKeys.ts +++ b/src/webapp/features/search/lib/searchKeys.ts @@ -17,4 +17,8 @@ export const searchKeys = { urlType, userId, ] as const, + profileSearch: (query: string) => + [...searchKeys.all(), 'profile', query] as const, + profileSearchInfinite: (query: string, limit?: number) => + [...searchKeys.profileSearch(query), 'infinite', limit] as const, };