feat: begin building out search results component

This commit is contained in:
Stevan Freeborn
2023-09-15 23:43:36 -05:00
parent 2162f87cfb
commit 18cb4fe0d9
7 changed files with 191 additions and 116 deletions
+1
View File
@@ -4,6 +4,7 @@
"codemirror",
"conve",
"doesn",
"Dtos",
"Keymap",
"lezer",
"nextjs",
+41 -99
View File
@@ -1,9 +1,9 @@
import { paginationOptsValidator } from 'convex/server';
import { v } from 'convex/values';
import { PostWithUserDto } from '../src/app/types';
import { Id } from './_generated/dataModel';
import { mutation, query } from './_generated/server';
import { userQuery } from './users';
import { Doc } from './_generated/dataModel.d';
import { QueryCtx, mutation, query } from './_generated/server';
import { createUserDto, userQuery } from './users';
export const createOrUpdatePost = mutation({
args: {
@@ -82,13 +82,7 @@ export const getPostById = query({
return {
...post,
user: {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
},
user: createUserDto(user),
};
},
});
@@ -102,36 +96,10 @@ export const getRepliesByParentId = query({
.order('desc')
.paginate(args.paginationOpts);
const repliesWithUserData = await Promise.all(
replies.page.map(async reply => {
const user = await ctx.db.get(reply.userId);
if (user === null) {
return {
...reply,
user: {
_id: '' as Id<'users'>,
_creationTime: 0,
clerkUsername: null,
clerkImageUrl: '',
clerkUserId: '',
},
};
}
return {
...reply,
user: {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
},
};
})
);
const repliesWithUserData = await getPostsWithUsers({
ctx,
posts: replies.page,
});
return { ...replies, page: repliesWithUserData };
},
});
@@ -170,35 +138,7 @@ export const getAllPostsWithUser = query({
.order('desc')
.paginate(args.paginationOpts);
const postsWithUser = await Promise.all(
posts.page.map(async post => {
const user = await ctx.db.get(post.userId);
if (user === null) {
return {
...post,
user: {
_id: '' as Id<'users'>,
_creationTime: 0,
clerkUsername: null,
clerkImageUrl: '',
clerkUserId: '',
},
};
}
return {
...post,
user: {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
},
};
})
);
const postsWithUser = await getPostsWithUsers({ ctx, posts: posts.page });
return { ...posts, page: postsWithUser };
},
@@ -239,35 +179,7 @@ export const getAllPostsForFollowings = query({
.order('desc')
.paginate(args.paginationOpts);
const postsWithUser = await Promise.all(
posts.page.map(async post => {
const user = await ctx.db.get(post.userId);
if (user === null) {
return {
...post,
user: {
_id: '' as Id<'users'>,
_creationTime: 0,
clerkUsername: null,
clerkImageUrl: '',
clerkUserId: '',
},
};
}
return {
...post,
user: {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
},
};
})
);
const postsWithUser = await getPostsWithUsers({ ctx, posts: posts.page });
return { ...posts, page: postsWithUser };
},
@@ -276,11 +188,41 @@ export const getAllPostsForFollowings = query({
export const getPostsBySearchTerm = query({
args: { term: v.string(), paginationOpts: paginationOptsValidator },
handler: async (ctx, args) => {
return await ctx.db
const posts = await ctx.db
.query('posts')
.withSearchIndex('search_by_content', q =>
q.search('content', args.term).eq('parentPostId', undefined)
)
.paginate(args.paginationOpts);
const postsWithUser = await getPostsWithUsers({ ctx, posts: posts.page });
return { ...posts, page: postsWithUser };
},
});
async function getPostsWithUsers({
ctx,
posts,
}: {
ctx: QueryCtx;
posts: Doc<'posts'>[];
}) {
return await Promise.all(
posts.map(async post => {
const user = await ctx.db.get(post.userId);
if (user === null) {
return {
...post,
user: createUserDto(user),
};
}
return {
...post,
user: createUserDto(user),
};
})
);
}
+1
View File
@@ -38,6 +38,7 @@ export default defineSchema(
.index('by_clerk_id', ['clerkUser.id'])
.searchIndex('search_by_username', {
searchField: 'clerkUser.username',
filterFields: [],
}),
posts: defineTable({
parentPostId: v.optional(v.id('posts')),
+28 -15
View File
@@ -1,6 +1,7 @@
import { paginationOptsValidator } from 'convex/server';
import { v } from 'convex/values';
import { UserDto } from './../src/app/types/index';
import { Doc, Id } from './_generated/dataModel';
import {
QueryCtx,
internalMutation,
@@ -24,13 +25,7 @@ export const getUserByClerkId = query({
return 'USER_NOT_FOUND';
}
return {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
};
return createUserDto(user);
},
});
@@ -43,13 +38,7 @@ export const getUserById = query({
return 'USER_NOT_FOUND';
}
return {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
};
return createUserDto(user);
},
});
@@ -126,11 +115,35 @@ export const deleteUser = internalMutation({
export const getUsersBySearchTerm = query({
args: { term: v.string(), paginationOpts: paginationOptsValidator },
handler: async (ctx, args) => {
return await ctx.db
const users = await ctx.db
.query('users')
.withSearchIndex('search_by_username', q =>
q.search('clerkUser.username', args.term)
)
.paginate(args.paginationOpts);
const userDtos = users.page.map(createUserDto);
return { ...users, page: userDtos };
},
});
export function createUserDto(user: Doc<'users'> | null) {
if (user === null) {
return {
_id: '' as Id<'users'>,
_creationTime: 0,
clerkUsername: null,
clerkImageUrl: '',
clerkUserId: '',
};
}
return {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
};
}
+12
View File
@@ -0,0 +1,12 @@
import SearchResults from '@/components/SearchResults';
export default function SearchPage({ params }: { params: { term: string } }) {
return (
<main className='flex flex-col items-center flex-1'>
<div className='flex flex-col w-full max-w-4xl gap-4'>
<h1 className='text-4xl font-bold'>Here&apos;s what we found</h1>
<SearchResults term={params.term} />
</div>
</main>
);
}
+19 -2
View File
@@ -1,5 +1,6 @@
'use client';
import { useRouter } from '@/hooks';
import { SignedIn, SignedOut, useUser } from '@clerk/nextjs';
import Link from 'next/link';
import { useEffect, useState } from 'react';
@@ -16,8 +17,10 @@ import ThemeButton from './ThemeButton';
import UserButton from './UserButton';
export default function Navbar() {
const [term, setTerm] = useState('');
const { user } = useUser();
const [navOpen, setNavOpen] = useState(false);
const router = useRouter();
useEffect(() => {
function handler() {
@@ -57,13 +60,27 @@ export default function Navbar() {
<ul className='flex flex-col w-full items-start gap-4 md:flex-row md:items-center md:max-w-min'>
<SignedIn>
<li className='flex w-full'>
<div className='flex w-full'>
<form
onSubmit={e => {
e.preventDefault();
router.push(`/search/${term}`);
}}
className='flex w-full relative'
>
<input
onChange={e => setTerm(e.target.value)}
value={term}
type='text'
placeholder='Search'
className='w-full md:w-[unset] py-1 px-2 rounded-md border border-gray-600 dark:bg-primary-gray dark:border-0'
/>
</div>
<button
disabled={!term || !term.trim()}
type='submit'
>
Search
</button>
</form>
</li>
<li>
<div className='flex items-center gap-2'>
+89
View File
@@ -0,0 +1,89 @@
'use client';
import { UsePaginatedQueryReturnType, usePaginatedQuery } from 'convex/react';
import { FunctionReference } from 'convex/server';
import { useState } from 'react';
import { api } from '../../convex/_generated/api';
import { Id } from '../../convex/_generated/dataModel';
import UserProfile from './UserProfile';
type UserSearchResultsPager = UsePaginatedQueryReturnType<
FunctionReference<
'query',
'public',
{
term: string;
paginationOpts: {
id?: number | undefined;
numItems: number;
cursor: string | null;
};
},
{
page: {
_id: Id<'users'>;
_creationTime: number;
clerkUsername: string | null;
clerkImageUrl: string;
clerkUserId: string;
}[];
isDone: boolean;
continueCursor: string;
}
>
>;
function UserSearchResults({ pager }: { pager: UserSearchResultsPager }) {
return (
<div>
<div>
{pager.results.map(user => (
<div key={user._id}>
<UserProfile user={user} />
</div>
))}
</div>
</div>
);
}
export default function SearchResults({ term }: { term: string }) {
const PAGE_SIZE = 10;
const [current, setCurrent] = useState<'posts' | 'users'>('posts');
const postsPager = usePaginatedQuery(
api.posts.getPostsBySearchTerm,
{ term },
{ initialNumItems: PAGE_SIZE }
);
const usersPager = usePaginatedQuery(
api.users.getUsersBySearchTerm,
{ term },
{ initialNumItems: PAGE_SIZE }
);
return (
<div>
<div className='flex gap-2 border-b border-gray-600'>
<button
onClick={() => setCurrent('posts')}
disabled={current === 'posts'}
type='button'
className='py-1 px-2 border-b-4 border-transparent disabled:border-primary-accent'
>
Posts
</button>
<button
onClick={() => setCurrent('users')}
disabled={current === 'users'}
type='button'
className='py-1 px-2 border-b-4 border-transparent disabled:border-primary-accent'
>
Users
</button>
</div>
{current === 'posts' ? 'Posts' : <UserSearchResults pager={usersPager} />}
</div>
);
}