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 }>;
// for use on profiles
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 resolvedSearchParams = await searchParams;
@ -56,21 +57,26 @@ export default async function MiiList({ searchParams, userId }: Props) {
const shownMiiCount = await prisma.mii.count({
where: {
...whereTags,
...where,
userId,
},
});
const miis = await prisma.mii.findMany({
where: {
...whereTags,
...where,
userId,
},
orderBy,
include: {
...userInclude,
likedBy: {
where: {
userId: Number(session?.user.id),
},
where: userId
? {
userId: Number(session?.user.id),
}
: {},
select: {
userId: true,
},
@ -119,39 +125,43 @@ export default async function MiiList({ searchParams, userId }: Props) {
</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">
{formattedMiis.map((mii) => (
<div
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"]} />
{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">
{formattedMiis.map((mii) => (
<div
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"]} />
<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}>
{mii.name}
</Link>
<div id="tags" className="flex gap-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
{mii.tags.map((tag) => (
<Link href={{ query: { tags: tag } }} key={tag}>
{tag}
</Link>
))}
</div>
<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}>
{mii.name}
</Link>
<div id="tags" className="flex gap-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
{mii.tags.map((tag) => (
<Link href={{ query: { tags: tag } }} key={tag}>
{tag}
</Link>
))}
</div>
<div className="mt-auto grid grid-cols-2 items-center">
<LikeButton likes={mii.likes} miiId={mii.id} isLiked={mii.isLikedByUser} isLoggedIn={session?.user != null} />
<div className="mt-auto grid grid-cols-2 items-center">
<LikeButton likes={mii.likes} miiId={mii.id} isLiked={mii.isLikedByUser} isLoggedIn={session?.user != null} />
{userId == null && (
<Link href={`/profile/${mii.user.id}`} className="text-sm text-right overflow-hidden text-ellipsis">
@{mii.user?.username}
</Link>
)}
{userId == null && (
<Link href={`/profile/${mii.user.id}`} className="text-sm text-right overflow-hidden text-ellipsis">
@{mii.user?.username}
</Link>
)}
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
) : (
<p className="text-xl text-center mt-10">No results found.</p>
)}
</div>
);
}

View file

@ -1,16 +1,45 @@
"use client";
import { useState } from "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() {
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 (
<div className="max-w-md w-full flex rounded-xl focus-within:ring-[3px] ring-orange-400/50 transition shadow-md">
<input
type="text"
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"
/>
<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" />
</button>
</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>
);
}