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' && (
+
+ )}
Search
- {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,
};