diff --git a/app/api/search/people/route.ts b/app/api/search/people/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c13552205befa1a4ac47150dcda6e9f3b097f2e --- /dev/null +++ b/app/api/search/people/route.ts @@ -0,0 +1,46 @@ +import { NextResponse } from "next/server" +import { z } from "zod" + +import { db } from "@/lib/db" + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url) + const query = searchParams.get("query") as string + + const querySchema = z.string().min(1) + const zod = querySchema.safeParse(query) + + if (!zod.success) { + return NextResponse.json(zod.error.formErrors, { status: 400 }) + } + + try { + const people = await db.user.findMany({ + where: { + OR: [{ + username: { + contains: query, + mode: "insensitive", + }, + }, + + { + name: { + contains: query, + mode: "insensitive", + }, + }], + }, + + take: 3, + + include: { + followers: true, + }, + }) + + return NextResponse.json(people, { status: 200 }) + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } +} \ No newline at end of file diff --git a/app/api/search/route.ts b/app/api/search/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..01046597952697fcabcbc3d43c7b1e21a7a27f3d --- /dev/null +++ b/app/api/search/route.ts @@ -0,0 +1,49 @@ +import { NextResponse } from "next/server" +import { z } from "zod" + +import { db } from "@/lib/db" + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url) + const query = searchParams.get("query") as string + + const querySchema = z.string().min(1) + const zod = querySchema.safeParse(query) + + if (!zod.success) { + return NextResponse.json(zod.error.formErrors, { status: 400 }) + } + + try { + const people = await db.user.findMany({ + where: { + OR: [{ + username: { + contains: query, + mode: "insensitive", + }, + }, + + { + name: { + contains: query, + mode: "insensitive", + }, + }], + }, + }) + + const hashtags = await db.hashtag.findMany({ + where: { + text: { + contains: query, + mode: "insensitive", + }, + }, + }) + + return NextResponse.json({ people, hashtags }, { status: 200 }) + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } +} \ No newline at end of file diff --git a/components/nav-header.tsx b/components/nav-header.tsx index fa9c7da9b6056ce35429edd5c8c94a279148163c..52f84ead956a77e460d95d8b889f1d05e50c2c8c 100644 --- a/components/nav-header.tsx +++ b/components/nav-header.tsx @@ -3,7 +3,7 @@ import Link from "next/link" import { cn } from "@/lib/utils" import { User } from "next-auth" import { GameUnityLogo } from "./logo" -import SearchInput from "./search-input" +import SearchInput from "./search/components/search-input" import { Button, buttonVariants } from "./ui/button" import { UserAccountDropdown } from "./user-nav" diff --git a/components/search/api/get-query-people.ts b/components/search/api/get-query-people.ts new file mode 100644 index 0000000000000000000000000000000000000000..6137c96481b51a4870d43fabcfc89b992851d18e --- /dev/null +++ b/components/search/api/get-query-people.ts @@ -0,0 +1,11 @@ +import axios from "axios" + +export const getQueryPeople = async (query: string | undefined) => { + try { + const { data } = await axios.get(`/api/search/people?query=${query}`) + + return data + } catch (error: any) { + return error.Message + } +} \ No newline at end of file diff --git a/components/search/api/get-search-results.ts b/components/search/api/get-search-results.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a606fe98f33dab81e77dfeb65c91c35fa54a910 --- /dev/null +++ b/components/search/api/get-search-results.ts @@ -0,0 +1,11 @@ +import axios from "axios" + +export const getSearchResults = async (query: string) => { + try { + const { data } = await axios.get(`/api/search?query=${query}`) + + return data + } catch (error: any) { + return error.message + } +} \ No newline at end of file diff --git a/components/search/components/no-results.tsx b/components/search/components/no-results.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d6d93ba46596c7d0c893076713d34bb6258557ee --- /dev/null +++ b/components/search/components/no-results.tsx @@ -0,0 +1,29 @@ +import Image from "next/image"; +import Link from "next/link"; + +import styles from "./styles/no-results.module.scss"; + +export const NoResults = ({ query }: { query: string | undefined }) => { + return ( + <div className={styles.container}> + <div className={styles.content}> + <div className={styles.image}> + <Image + src={`/no-results.png`} + alt="no results" + width={320} + height={160} + quality={100} + loading="lazy" + /> + </div> + <h1>No results for "{query}"</h1> + <p> + Try searching for something else, or check your{" "} + <Link href="/settings">Search settings</Link> to see if they’re + protecting you from potentially sensitive content. + </p> + </div> + </div> + ); +}; diff --git a/components/search-input.tsx b/components/search/components/search-input.tsx similarity index 67% rename from components/search-input.tsx rename to components/search/components/search-input.tsx index 5232ea1bf16a8ba4de5bf0eed919bcbe0b78365a..583422687c484c9ad6dbb919fedcf9e765345146 100644 --- a/components/search-input.tsx +++ b/components/search/components/search-input.tsx @@ -1,12 +1,14 @@ "use client" +import { Icons } from '@/components/icons' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' -import { usePathname, useRouter } from 'next/navigation' +import { zodResolver } from '@hookform/resolvers/zod' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' -import { Icons } from './icons' -import { Button } from './ui/button' -import { Input } from './ui/input' -import { toast } from './ui/use-toast' +import { useForm } from 'react-hook-form' +import { z } from 'zod' interface DocsSearchProps extends React.HTMLAttributes<HTMLFormElement> { } @@ -14,6 +16,15 @@ export default function SearchInput({ className, ...props }: DocsSearchProps) { const [searchQuery, setSearchQuery] = useState("") const router = useRouter() const pathname = usePathname() + const searchParams = useSearchParams() + + const search = searchParams?.get("query")?.toLowerCase() || "" + + const [query, setQuery] = useState( + pathname?.split("/")[1] === "search" + ? decodeURIComponent(search) + : "", + ) function onSearch(event: React.FormEvent) { event.preventDefault() @@ -27,10 +38,7 @@ export default function SearchInput({ className, ...props }: DocsSearchProps) { const encoededQuery = encodeURIComponent(searchQuery) router.push(`${pathname}?search=${encoededQuery}`) } else { - return toast({ - title: "Work in Progress!", - description: "Sorry, but global search is not available yet... ㅤYou can test it out in the games page though!", - }) + router.push(`/search?query=${searchQuery}`) } }; diff --git a/components/search/components/search-results.tsx b/components/search/components/search-results.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dc07b44a3982b6c7e4b0df9162f6731b820c9cf8 --- /dev/null +++ b/components/search/components/search-results.tsx @@ -0,0 +1,58 @@ +import { useSearchParams } from "next/navigation" + +import { InfiniteGweets } from "@/components/gweets/components/infinite-gweets" +import { useGweets } from "@/components/gweets/hooks/use-gweets" +import LoadingItem from "@/components/loading-item" +import { UserItem } from "@/components/profile/components/user-item" +import { TryAgain } from "@/components/try-again" +import { useSearchPeople } from "../hooks/use-search-people" +import { NoResults } from "./no-results" + +export const SearchResults = () => { + const searchParams = useSearchParams() + const query = decodeURIComponent(searchParams?.get("query") || "") + + const gweets = useGweets({ + queryKey: ["gweets", "query: ", query], + type: "search", + id: query, + }) + + const people = useSearchPeople(query) + + if (gweets.isLoading || gweets.isFetching) return <LoadingItem /> + + if (gweets.isError) return <TryAgain /> + + return ( + <div className=""> + {gweets?.data?.pages && + gweets?.data?.pages[0]?.gweets?.length === 0 && + people?.data?.length === 0 ? ( + <NoResults query={query} /> + ) : ( + <div className=""> + {people?.isSuccess && people?.data?.length > 0 && ( + <div className=""> + <h1>People</h1> + {people?.data?.map((person) => { + return <UserItem key={person?.id} user={person} sessionId={undefined} /> + })} + <button className="">View All</button> + </div> + )} + + <div className=""> + <InfiniteGweets + gweets={gweets?.data} + hasNextPage={gweets?.hasNextPage} + fetchNextPage={gweets?.fetchNextPage} + isSuccess={gweets?.isSuccess} + isFetchingNextPage={gweets?.isFetchingNextPage} + /> + </div> + </div> + )} + </div> + ) +} \ No newline at end of file diff --git a/components/search/hooks/use-search-people.ts b/components/search/hooks/use-search-people.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f37962f1c9ba9965bd04c97f28b1792bdd71798 --- /dev/null +++ b/components/search/hooks/use-search-people.ts @@ -0,0 +1,21 @@ +import { IUser } from "@/components/profile/types" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { getQueryPeople } from "../api/get-query-people" + +export const useSearchPeople = (query: string | undefined) => { + const queryClient = useQueryClient() + + return useQuery<IUser[]>( + ["people", "query: ", query], + async () => { + return getQueryPeople(query) + }, + { + refetchOnWindowFocus: false, + onSuccess: (data) => { + queryClient.setQueryData(["hashtag-people"], data) + }, + enabled: !!query, + }, + ) +} \ No newline at end of file diff --git a/components/search/hooks/use-search.ts b/components/search/hooks/use-search.ts new file mode 100644 index 0000000000000000000000000000000000000000..d3e20a5f8b56fab6691c0792a8fad42b04246487 --- /dev/null +++ b/components/search/hooks/use-search.ts @@ -0,0 +1,20 @@ +import { IUser } from "@/components/profile/types" +import { IHashtag } from "@/components/trends/types" +import { useQuery } from "@tanstack/react-query" +import { getSearchResults } from "../api/get-search-results" + +export const useSearch = (query: string) => { + return useQuery<{ + people: IUser[] + hashtags: IHashtag[] + }>( + ["search", query], + async () => { + return getSearchResults(query) + }, + { + refetchOnWindowFocus: false, + enabled: !!query, + }, + ) +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9ac6fb759ec50c66b95b3ccc21d7c3b0c93ddd37..aaa77bde0066aebff1711eca290088da92ff6d04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@t3-oss/env-nextjs": "^0.6.0", "@tanstack/react-query": "^4.29.19", "@uploadthing/react": "^5.1.0", + "axios": "^1.4.0", "bcrypt": "^5.1.0", "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", @@ -56,7 +57,7 @@ "@testing-library/react": "^14.0.0", "@types/bcrypt": "^5.0.0", "@types/jest": "^29.5.2", - "@types/node": "^20.4.0", + "@types/node": "^20.4.1", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@types/swagger-ui-react": "^4.18.0", @@ -3346,9 +3347,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.0.tgz", - "integrity": "sha512-jfT7iTf/4kOQ9S7CHV9BIyRaQqHu67mOjsIQBC3BKZvzvUB6zLxEwJ6sBE3ozcvP8kF6Uk5PXN0Q+c0dfhGX0g==", + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz", + "integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==", "dev": true }, "node_modules/@types/prettier": { diff --git a/package.json b/package.json index 453dfa6e70a2bfb4f0321f725a97d27496c824da..3c732185bbbf22def9135de92e764241cc542ed8 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@t3-oss/env-nextjs": "^0.6.0", "@tanstack/react-query": "^4.29.19", "@uploadthing/react": "^5.1.0", + "axios": "^1.4.0", "bcrypt": "^5.1.0", "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", @@ -61,7 +62,7 @@ "@testing-library/react": "^14.0.0", "@types/bcrypt": "^5.0.0", "@types/jest": "^29.5.2", - "@types/node": "^20.4.0", + "@types/node": "^20.4.1", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@types/swagger-ui-react": "^4.18.0", @@ -79,4 +80,4 @@ "semver": "^7.5.3", "optionator": "^0.9.3" } -} \ No newline at end of file +}