feat: search functionality
This commit is contained in:
parent
8f731dd358
commit
fb399734c0
3 changed files with 110 additions and 33 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
38
src/app/search/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue