Skip to content

Commit

Permalink
✨ Add people tab on search page
Browse files Browse the repository at this point in the history
  • Loading branch information
foysalit committed Nov 23, 2024
1 parent 1176067 commit 76fb942
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 67 deletions.
111 changes: 74 additions & 37 deletions app/search/page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import { Loading } from '@/common/Loader'
import { LoadMoreButton } from '@/common/LoadMoreButton'
import { PostAsCard } from '@/common/posts/PostsFeed'
import { useWorkspaceOpener } from '@/common/useWorkspaceOpener'
import { ProfileCard } from '@/repositories/AccountView'
import { WorkspacePanel } from '@/workspace/Panel'
import { AppBskyActorDefs, AppBskyFeedDefs } from '@atproto/api'
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'
import { useContentSearch } from 'components/search-content/useContentSearch'
import {
isActorData,
SearchContentSection,
useContentSearch,
} from 'components/search-content/useContentSearch'
import { SectionHeader } from 'components/SectionHeader'
import { useSearchParams } from 'next/navigation'
import { useTitle } from 'react-use'
Expand All @@ -22,25 +28,55 @@ const TABS = [
name: 'Latest',
href: `/search?section=latest`,
},
{
key: 'people',
name: 'People',
href: `/search?section=people`,
},
]

export const SearchPageContent = () => {
const searchParams = useSearchParams()
const term = searchParams.get('term') ?? ''
const section = searchParams.get('section') ?? 'top'
const section = (searchParams.get('section') ?? 'top') as SearchContentSection

let pageTitle = `Search Content`
if (term) {
pageTitle += ` - ${term}`
}

useTitle(pageTitle)
const { posts, isLoading, fetchNextPage, hasNextPage, addToWorkspace } =
const { data, isLoading, fetchNextPage, hasNextPage, addToWorkspace } =
useContentSearch({
term,
section,
})
const { toggleWorkspacePanel, isWorkspaceOpen } = useWorkspaceOpener()
const workspaceOptions = [
{
id: 'add_users_to_workspace',
text: section === 'people' ? 'Add all people' : 'Add all posters',
onClick: () => {
if (isActorData(data)) {
addToWorkspace(data.map((item) => item.did))
} else {
addToWorkspace(data.map((item) => item.author.did))
}
},
},
]

if (section !== 'people') {
workspaceOptions.push({
id: 'add_posts_to_workspace',
text: 'Add all posts',
onClick: () => {
if (!isActorData(data)) {
addToWorkspace(data.map((item) => item.uri))
}
},
})
}

return (
<>
Expand All @@ -50,48 +86,49 @@ export const SearchPageContent = () => {
containerClassName="inline-block"
rightAligned
className="inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-400 bg-white dark:bg-slate-800 dark:text-gray-100 dark px-4 py-2 text-sm text-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-slate-700"
items={[
{
id: 'add_users_to_workspace',
text: 'Add all posters',
onClick: () =>
addToWorkspace(posts.map((post) => post.author.did)),
},
{
id: 'add_posts_to_workspace',
text: 'Add all posts',
onClick: () => addToWorkspace(posts.map((post) => post.uri)),
},
]}
items={workspaceOptions}
>
Add to workspace
</Dropdown>
</div>
</SectionHeader>

<div className="w-5/6 sm:w-3/4 md:w-2/3 lg:w-1/2 mx-auto my-4 dark:text-gray-100">
{!term && (
{!term ? (
<div className="w-5/6 sm:w-3/4 md:w-2/3 lg:w-1/2 mx-auto my-4 dark:text-gray-100">
<EmptyDataset message="Please input a keyword on the top search bar">
<MagnifyingGlassIcon className="h-10 w-10" />
</EmptyDataset>
)}

{!!term && isLoading && <Loading />}
{posts.map((post) => (
<div className="mb-4" key={post.uri}>
<PostAsCard
className="bg-transparent px-3 py-2"
item={{ post }}
dense
/>
</div>
))}
{hasNextPage && (
<div className="flex justify-center mb-4">
<LoadMoreButton onClick={() => fetchNextPage()} />
</div>
)}
</div>
</div>
) : (
<div className="mx-auto">
{isLoading && <Loading />}
{isActorData(data) ? (
<div className="my-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
{data.map((item) => {
return <ProfileCard profile={item} key={item.did} />
})}
</div>
) : (
<div className="w-5/6 sm:w-3/4 md:w-2/3 lg:w-1/2 mx-auto my-4 dark:text-gray-100">
{data.map((item) => {
return (
<div className="mb-4" key={item.uri}>
<PostAsCard
className="bg-transparent px-3 py-2"
item={{ post: item }}
dense
/>
</div>
)
})}
</div>
)}
{hasNextPage && (
<div className="flex justify-center mb-4">
<LoadMoreButton onClick={() => fetchNextPage()} />
</div>
)}
</div>
)}

<WorkspacePanel
open={isWorkspaceOpen}
Expand Down
55 changes: 30 additions & 25 deletions components/repositories/AccountView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -802,36 +802,41 @@ export function AccountsGrid({
)}
<div className="mt-1 grid grid-cols-1 gap-4 sm:grid-cols-2">
{accounts?.map((account) => (
<div
key={account.handle}
className="relative flex items-center space-x-3 rounded-lg border border-gray-300 dark:border-slate-700 bg-white dark:bg-slate-800 px-6 py-5 shadow-sm dark:shadow-slate-800 focus-within:ring-2 focus-within:ring-pink-500 focus-within:ring-teal-500 focus-within:ring-offset-2 hover:border-gray-400 dark:hover:border-slate-700"
>
<div className="flex-shrink-0">
<ProfileAvatar
className="h-10 w-10 rounded-full"
profile={account}
/>
</div>
<div className="min-w-0 flex-1">
<Link
href={`/repositories/${account.did}`}
className="focus:outline-none"
>
<span className="absolute inset-0" aria-hidden="true" />
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
{account.displayName || `@${account.handle}`}
</p>
<p className="truncate text-sm text-gray-500 dark:text-gray-50">
@{account.handle}
</p>
</Link>
</div>
</div>
<ProfileCard profile={account} key={account.handle} />
))}
</div>
</div>
)
}

export const ProfileCard = ({
profile,
}: {
profile: AppBskyActorDefs.ProfileView | AppBskyActorDefs.ProfileViewBasic
}) => {
return (
<div className="relative flex items-center space-x-3 rounded-lg border border-gray-300 dark:border-slate-700 bg-white dark:bg-slate-800 px-6 py-5 shadow-sm dark:shadow-slate-800 focus-within:ring-2 focus-within:ring-pink-500 focus-within:ring-teal-500 focus-within:ring-offset-2 hover:border-gray-400 dark:hover:border-slate-700">
<div className="flex-shrink-0">
<ProfileAvatar className="h-10 w-10 rounded-full" profile={profile} />
</div>
<div className="min-w-0 flex-1">
<Link
href={`/repositories/${profile.did}`}
className="focus:outline-none"
>
<span className="absolute inset-0" aria-hidden="true" />
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
{profile.displayName || `@${profile.handle}`}
</p>
<p className="truncate text-sm text-gray-500 dark:text-gray-50">
@{profile.handle}
</p>
</Link>
</div>
</div>
)
}

enum EventViews {
ByUser,
ForUser,
Expand Down
54 changes: 49 additions & 5 deletions components/search-content/useContentSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,53 @@
import { useLabelerAgent } from '@/shell/ConfigurationContext'
import { useAppviewAgent, useLabelerAgent } from '@/shell/ConfigurationContext'
import { useWorkspaceAddItemsMutation } from '@/workspace/hooks'
import {
AppBskyActorDefs,
AppBskyActorSearchActors,
AppBskyFeedDefs,
AppBskyFeedSearchPosts,
} from '@atproto/api'
import { useInfiniteQuery } from '@tanstack/react-query'

type ActorResult = AppBskyActorSearchActors.OutputSchema
type PostResult = AppBskyFeedSearchPosts.OutputSchema
export type SearchContentSection = 'people' | 'top' | 'latest'
type QueryData<S extends SearchContentSection> = S extends 'people'
? ActorResult
: PostResult

export const useContentSearch = ({
term,
section,
}: {
term: string
section: string
section: SearchContentSection
}) => {
const labelerAgent = useLabelerAgent()
const appviewAgent = useAppviewAgent()

const { mutate: addToWorkspace } = useWorkspaceAddItemsMutation()

const searchInfo = useInfiniteQuery({
const searchInfo = useInfiniteQuery<QueryData<typeof section>>({
queryKey: ['searchContent', { term, section }],
enabled: !!term,
queryFn: async ({ pageParam }) => {
if (section === 'people') {
// This is actually never hit, we always have appview configured
if (!appviewAgent) {
throw new Error(
'Can not search actors without appview agent configured',
)
}

const { data } = await appviewAgent.app.bsky.actor.searchActors({
q: term,
limit: 30,
cursor: pageParam,
})

return data
}

const { data } = await labelerAgent.app.bsky.feed.searchPosts({
q: term,
limit: 30,
Expand All @@ -28,11 +59,24 @@ export const useContentSearch = ({
getNextPageParam: (lastPage) => lastPage.cursor,
})

const posts = searchInfo.data?.pages.flatMap((page) => page.posts) || []
const data =
(section === 'people'
? (searchInfo.data?.pages.flatMap(
(page) => page.actors,
) as ActorResult['actors'])
: (searchInfo.data?.pages.flatMap(
(page) => page.posts,
) as PostResult['posts'])) || []

return {
...searchInfo,
posts,
data,
addToWorkspace,
}
}

export function isActorData(
data: ActorResult['actors'] | PostResult['posts'],
): data is ActorResult['actors'] {
return data.length === 0 || 'did' in data[0]
}

0 comments on commit 76fb942

Please sign in to comment.