feat: search functionality

This commit is contained in:
trafficlunar 2025-03-31 20:58:05 +01:00
parent 8f731dd358
commit fb399734c0
3 changed files with 110 additions and 33 deletions

View file

@ -11,9 +11,10 @@ interface Props {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
// for use on profiles // for use on profiles
userId?: number; userId?: number;
where?: Record<string, any>;
} }
export default async function MiiList({ searchParams, userId }: Props) { export default async function MiiList({ searchParams, userId, where }: Props) {
const session = await auth(); const session = await auth();
const resolvedSearchParams = await searchParams; const resolvedSearchParams = await searchParams;
@ -56,21 +57,26 @@ export default async function MiiList({ searchParams, userId }: Props) {
const shownMiiCount = await prisma.mii.count({ const shownMiiCount = await prisma.mii.count({
where: { where: {
...whereTags, ...whereTags,
...where,
userId, userId,
}, },
}); });
const miis = await prisma.mii.findMany({ const miis = await prisma.mii.findMany({
where: { where: {
...whereTags, ...whereTags,
...where,
userId, userId,
}, },
orderBy, orderBy,
include: { include: {
...userInclude, ...userInclude,
likedBy: { likedBy: {
where: { where: userId
? {
userId: Number(session?.user.id), userId: Number(session?.user.id),
}, }
: {},
select: { select: {
userId: true, userId: true,
}, },
@ -119,6 +125,7 @@ export default async function MiiList({ searchParams, userId }: Props) {
</div> </div>
</div> </div>
{miis.length > 0 ? (
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-sm:grid-cols-2 max-[25rem]:grid-cols-1"> <div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-sm:grid-cols-2 max-[25rem]:grid-cols-1">
{formattedMiis.map((mii) => ( {formattedMiis.map((mii) => (
<div <div
@ -152,6 +159,9 @@ export default async function MiiList({ searchParams, userId }: Props) {
</div> </div>
))} ))}
</div> </div>
) : (
<p className="text-xl text-center mt-10">No results found.</p>
)}
</div> </div>
); );
} }

View file

@ -1,16 +1,45 @@
"use client"; "use client";
import { useState } from "react";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { redirect } from "next/navigation";
import { z } from "zod";
const searchSchema = z
.string()
.trim()
.min(2)
.max(64)
.regex(/^[a-zA-Z0-9_]+$/);
export default function SearchBar() { export default function SearchBar() {
const [query, setQuery] = useState("");
const handleSearch = () => {
const result = searchSchema.safeParse(query);
if (!result.success) redirect("/");
redirect(`/search?q=${query}`);
};
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Enter") handleSearch();
};
return ( return (
<div className="max-w-md w-full flex rounded-xl focus-within:ring-[3px] ring-orange-400/50 transition shadow-md"> <div className="max-w-md w-full flex rounded-xl focus-within:ring-[3px] ring-orange-400/50 transition shadow-md">
<input <input
type="text" type="text"
placeholder="Search..." placeholder="Search..."
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
className="bg-orange-200 border-2 border-orange-400 py-2 px-3 rounded-l-xl outline-0 w-full placeholder:text-black/40" className="bg-orange-200 border-2 border-orange-400 py-2 px-3 rounded-l-xl outline-0 w-full placeholder:text-black/40"
/> />
<button className="bg-orange-400 p-2 w-12 rounded-r-xl flex justify-center items-center cursor-pointer text-2xl transition-all hover:text-[1.75rem] active:text-2xl"> <button
onClick={handleSearch}
className="bg-orange-400 p-2 w-12 rounded-r-xl flex justify-center items-center cursor-pointer text-2xl transition-all hover:text-[1.75rem] active:text-2xl"
>
<Icon icon="ic:baseline-search" /> <Icon icon="ic:baseline-search" />
</button> </button>
</div> </div>

38
src/app/search/page.tsx Normal file
View file

@ -0,0 +1,38 @@
import { notFound } from "next/navigation";
import { z } from "zod";
import MiiList from "../components/mii-list";
interface Props {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
const searchSchema = z
.string()
.trim()
.min(2)
.max(64)
.regex(/^[a-zA-Z0-9_]+$/);
export default async function SearchPage({ searchParams }: Props) {
const { q: rawQuery } = await searchParams;
const result = searchSchema.safeParse(rawQuery);
if (!result.success) notFound();
const query = result.data.toLowerCase();
return (
<div>
<p className="text-lg">
Search results for "<span className="font-bold">{query}</span>"
</p>
<MiiList
searchParams={searchParams}
where={{
OR: [{ name: { contains: query, mode: "insensitive" } }, { tags: { has: query } }],
}}
/>
</div>
);
}