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", "codemirror",
"conve", "conve",
"doesn", "doesn",
"Dtos",
"Keymap", "Keymap",
"lezer", "lezer",
"nextjs", "nextjs",
+41 -99
View File
@@ -1,9 +1,9 @@
import { paginationOptsValidator } from 'convex/server'; import { paginationOptsValidator } from 'convex/server';
import { v } from 'convex/values'; import { v } from 'convex/values';
import { PostWithUserDto } from '../src/app/types'; import { PostWithUserDto } from '../src/app/types';
import { Id } from './_generated/dataModel'; import { Doc } from './_generated/dataModel.d';
import { mutation, query } from './_generated/server'; import { QueryCtx, mutation, query } from './_generated/server';
import { userQuery } from './users'; import { createUserDto, userQuery } from './users';
export const createOrUpdatePost = mutation({ export const createOrUpdatePost = mutation({
args: { args: {
@@ -82,13 +82,7 @@ export const getPostById = query({
return { return {
...post, ...post,
user: { user: createUserDto(user),
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
},
}; };
}, },
}); });
@@ -102,36 +96,10 @@ export const getRepliesByParentId = query({
.order('desc') .order('desc')
.paginate(args.paginationOpts); .paginate(args.paginationOpts);
const repliesWithUserData = await Promise.all( const repliesWithUserData = await getPostsWithUsers({
replies.page.map(async reply => { ctx,
const user = await ctx.db.get(reply.userId); posts: replies.page,
});
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,
},
};
})
);
return { ...replies, page: repliesWithUserData }; return { ...replies, page: repliesWithUserData };
}, },
}); });
@@ -170,35 +138,7 @@ export const getAllPostsWithUser = query({
.order('desc') .order('desc')
.paginate(args.paginationOpts); .paginate(args.paginationOpts);
const postsWithUser = await Promise.all( const postsWithUser = await getPostsWithUsers({ ctx, posts: posts.page });
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,
},
};
})
);
return { ...posts, page: postsWithUser }; return { ...posts, page: postsWithUser };
}, },
@@ -239,35 +179,7 @@ export const getAllPostsForFollowings = query({
.order('desc') .order('desc')
.paginate(args.paginationOpts); .paginate(args.paginationOpts);
const postsWithUser = await Promise.all( const postsWithUser = await getPostsWithUsers({ ctx, posts: posts.page });
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,
},
};
})
);
return { ...posts, page: postsWithUser }; return { ...posts, page: postsWithUser };
}, },
@@ -276,11 +188,41 @@ export const getAllPostsForFollowings = query({
export const getPostsBySearchTerm = query({ export const getPostsBySearchTerm = query({
args: { term: v.string(), paginationOpts: paginationOptsValidator }, args: { term: v.string(), paginationOpts: paginationOptsValidator },
handler: async (ctx, args) => { handler: async (ctx, args) => {
return await ctx.db const posts = await ctx.db
.query('posts') .query('posts')
.withSearchIndex('search_by_content', q => .withSearchIndex('search_by_content', q =>
q.search('content', args.term).eq('parentPostId', undefined) q.search('content', args.term).eq('parentPostId', undefined)
) )
.paginate(args.paginationOpts); .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']) .index('by_clerk_id', ['clerkUser.id'])
.searchIndex('search_by_username', { .searchIndex('search_by_username', {
searchField: 'clerkUser.username', searchField: 'clerkUser.username',
filterFields: [],
}), }),
posts: defineTable({ posts: defineTable({
parentPostId: v.optional(v.id('posts')), parentPostId: v.optional(v.id('posts')),
+28 -15
View File
@@ -1,6 +1,7 @@
import { paginationOptsValidator } from 'convex/server'; import { paginationOptsValidator } from 'convex/server';
import { v } from 'convex/values'; import { v } from 'convex/values';
import { UserDto } from './../src/app/types/index'; import { UserDto } from './../src/app/types/index';
import { Doc, Id } from './_generated/dataModel';
import { import {
QueryCtx, QueryCtx,
internalMutation, internalMutation,
@@ -24,13 +25,7 @@ export const getUserByClerkId = query({
return 'USER_NOT_FOUND'; return 'USER_NOT_FOUND';
} }
return { return createUserDto(user);
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
};
}, },
}); });
@@ -43,13 +38,7 @@ export const getUserById = query({
return 'USER_NOT_FOUND'; return 'USER_NOT_FOUND';
} }
return { return createUserDto(user);
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
};
}, },
}); });
@@ -126,11 +115,35 @@ export const deleteUser = internalMutation({
export const getUsersBySearchTerm = query({ export const getUsersBySearchTerm = query({
args: { term: v.string(), paginationOpts: paginationOptsValidator }, args: { term: v.string(), paginationOpts: paginationOptsValidator },
handler: async (ctx, args) => { handler: async (ctx, args) => {
return await ctx.db const users = await ctx.db
.query('users') .query('users')
.withSearchIndex('search_by_username', q => .withSearchIndex('search_by_username', q =>
q.search('clerkUser.username', args.term) q.search('clerkUser.username', args.term)
) )
.paginate(args.paginationOpts); .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'; 'use client';
import { useRouter } from '@/hooks';
import { SignedIn, SignedOut, useUser } from '@clerk/nextjs'; import { SignedIn, SignedOut, useUser } from '@clerk/nextjs';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -16,8 +17,10 @@ import ThemeButton from './ThemeButton';
import UserButton from './UserButton'; import UserButton from './UserButton';
export default function Navbar() { export default function Navbar() {
const [term, setTerm] = useState('');
const { user } = useUser(); const { user } = useUser();
const [navOpen, setNavOpen] = useState(false); const [navOpen, setNavOpen] = useState(false);
const router = useRouter();
useEffect(() => { useEffect(() => {
function handler() { 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'> <ul className='flex flex-col w-full items-start gap-4 md:flex-row md:items-center md:max-w-min'>
<SignedIn> <SignedIn>
<li className='flex w-full'> <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 <input
onChange={e => setTerm(e.target.value)}
value={term}
type='text' type='text'
placeholder='Search' 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' 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>
<li> <li>
<div className='flex items-center gap-2'> <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>
);
}