From f63da9792dd1249024c9c0ee5fec9442b8ff315b Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 15 Sep 2023 22:16:21 -0500 Subject: [PATCH 1/6] feat: add queries to search for users by username and for parent posts based on content --- convex/posts.ts | 14 ++++++++++++-- convex/schema.ts | 12 ++++++++++-- convex/users.ts | 13 +++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/convex/posts.ts b/convex/posts.ts index 1e04167..278c537 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -1,4 +1,4 @@ -import { PaginationResult, paginationOptsValidator } from 'convex/server'; +import { paginationOptsValidator } from 'convex/server'; import { v } from 'convex/values'; import { PostWithUserDto } from '../src/app/types'; import { Id } from './_generated/dataModel'; @@ -206,7 +206,7 @@ export const getAllPostsWithUser = query({ export const getAllPostsForFollowings = query({ args: { paginationOpts: paginationOptsValidator }, - handler: async (ctx, args): Promise> => { + handler: async (ctx, args) => { const currentUser = await ctx.auth.getUserIdentity(); if (currentUser === null) { @@ -272,3 +272,13 @@ export const getAllPostsForFollowings = query({ return { ...posts, page: postsWithUser }; }, }); + +export const getPostsBySearchTerm = query({ + args: { term: v.string(), paginationOpts: paginationOptsValidator }, + handler: async (ctx, args) => { + return await ctx.db + .query('posts') + .withSearchIndex('search_by_content', q => q.search('content', args.term)) + .paginate(args.paginationOpts); + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts index 8d40c56..3784e03 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -34,14 +34,22 @@ export default defineSchema( created_at: v.number(), updated_at: v.number(), }), - }).index('by_clerk_id', ['clerkUser.id']), + }) + .index('by_clerk_id', ['clerkUser.id']) + .searchIndex('search_by_username', { + searchField: 'clerkUser.username', + }), posts: defineTable({ parentPostId: v.optional(v.id('posts')), userId: v.id('users'), content: v.array(v.string()), }) .index('by_user_id', ['userId']) - .index('by_parent_id', ['parentPostId']), + .index('by_parent_id', ['parentPostId']) + .searchIndex('search_by_content', { + searchField: 'content', + filterFields: ['parentPostId'], + }), likes: defineTable({ postId: v.id('posts'), userId: v.id('users'), diff --git a/convex/users.ts b/convex/users.ts index b90f9c1..8dbb8d6 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -1,3 +1,4 @@ +import { paginationOptsValidator } from 'convex/server'; import { v } from 'convex/values'; import { UserDto } from './../src/app/types/index'; import { @@ -121,3 +122,15 @@ export const deleteUser = internalMutation({ await ctx.db.delete(userRecord._id); }, }); + +export const getUsersBySearchTerm = query({ + args: { term: v.string(), paginationOpts: paginationOptsValidator }, + handler: async (ctx, args) => { + return await ctx.db + .query('users') + .withSearchIndex('search_by_username', q => + q.search('clerkUser.username', args.term) + ) + .paginate(args.paginationOpts); + }, +}); From 2162f87cfbddba0147b4a8f0cca4250dbe606920 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 15 Sep 2023 22:17:03 -0500 Subject: [PATCH 2/6] fix: filter search content by posts that are not children --- convex/posts.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/convex/posts.ts b/convex/posts.ts index 278c537..581dfe6 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -278,7 +278,9 @@ export const getPostsBySearchTerm = query({ handler: async (ctx, args) => { return await ctx.db .query('posts') - .withSearchIndex('search_by_content', q => q.search('content', args.term)) + .withSearchIndex('search_by_content', q => + q.search('content', args.term).eq('parentPostId', undefined) + ) .paginate(args.paginationOpts); }, }); From 18cb4fe0d93aad7465fa57b1f539e630a49ccf5f Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 15 Sep 2023 23:43:36 -0500 Subject: [PATCH 3/6] feat: begin building out search results component --- .vscode/settings.json | 1 + convex/posts.ts | 140 +++++++++---------------------- convex/schema.ts | 1 + convex/users.ts | 43 ++++++---- src/app/search/[term]/page.tsx | 12 +++ src/components/Navbar.tsx | 21 ++++- src/components/SearchResults.tsx | 89 ++++++++++++++++++++ 7 files changed, 191 insertions(+), 116 deletions(-) create mode 100644 src/app/search/[term]/page.tsx create mode 100644 src/components/SearchResults.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index c5c3a86..3737ea8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "codemirror", "conve", "doesn", + "Dtos", "Keymap", "lezer", "nextjs", diff --git a/convex/posts.ts b/convex/posts.ts index 581dfe6..c9331fa 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -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), + }; + }) + ); +} diff --git a/convex/schema.ts b/convex/schema.ts index 3784e03..bb58827 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -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')), diff --git a/convex/users.ts b/convex/users.ts index 8dbb8d6..cc8bf7f 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -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, + }; +} diff --git a/src/app/search/[term]/page.tsx b/src/app/search/[term]/page.tsx new file mode 100644 index 0000000..7a04868 --- /dev/null +++ b/src/app/search/[term]/page.tsx @@ -0,0 +1,12 @@ +import SearchResults from '@/components/SearchResults'; + +export default function SearchPage({ params }: { params: { term: string } }) { + return ( +
+
+

Here's what we found

+ +
+
+ ); +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index af9e54f..3eb50fe 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -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() {