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,39 +125,43 @@ export default async function MiiList({ searchParams, userId }: Props) {
</div> </div>
</div> </div>
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-sm:grid-cols-2 max-[25rem]:grid-cols-1"> {miis.length > 0 ? (
{formattedMiis.map((mii) => ( <div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-sm:grid-cols-2 max-[25rem]:grid-cols-1">
<div {formattedMiis.map((mii) => (
key={mii.id} <div
className="flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3 transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600" key={mii.id}
> className="flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3 transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600"
<Carousel images={["https://placehold.co/600x400", "https://placehold.co/600x400", "https://placehold.co/600x400"]} /> >
<Carousel images={["https://placehold.co/600x400", "https://placehold.co/600x400", "https://placehold.co/600x400"]} />
<div className="p-4 flex flex-col gap-1 h-full"> <div className="p-4 flex flex-col gap-1 h-full">
<Link href={`/mii/${mii.id}`} className="font-bold text-2xl overflow-hidden text-ellipsis line-clamp-2" title={mii.name}> <Link href={`/mii/${mii.id}`} className="font-bold text-2xl overflow-hidden text-ellipsis line-clamp-2" title={mii.name}>
{mii.name} {mii.name}
</Link> </Link>
<div id="tags" className="flex gap-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs"> <div id="tags" className="flex gap-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
{mii.tags.map((tag) => ( {mii.tags.map((tag) => (
<Link href={{ query: { tags: tag } }} key={tag}> <Link href={{ query: { tags: tag } }} key={tag}>
{tag} {tag}
</Link> </Link>
))} ))}
</div> </div>
<div className="mt-auto grid grid-cols-2 items-center"> <div className="mt-auto grid grid-cols-2 items-center">
<LikeButton likes={mii.likes} miiId={mii.id} isLiked={mii.isLikedByUser} isLoggedIn={session?.user != null} /> <LikeButton likes={mii.likes} miiId={mii.id} isLiked={mii.isLikedByUser} isLoggedIn={session?.user != null} />
{userId == null && ( {userId == null && (
<Link href={`/profile/${mii.user.id}`} className="text-sm text-right overflow-hidden text-ellipsis"> <Link href={`/profile/${mii.user.id}`} className="text-sm text-right overflow-hidden text-ellipsis">
@{mii.user?.username} @{mii.user?.username}
</Link> </Link>
)} )}
</div>
</div> </div>
</div> </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>
);
}