- Fix proxy routes to use resolveImagerBase instead of removed resolveUpstreamBase - Fix avatarImageUrl type signature to use AvatarOptions - Run biome formatter
232 lines
5.9 KiB
TypeScript
232 lines
5.9 KiB
TypeScript
import { and, eq, like } from "drizzle-orm";
|
|
import type { Metadata } from "next";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { db, Rooms, User } from "@/lib/db";
|
|
import { avatarImageUrl } from "@/lib/format";
|
|
|
|
export const metadata: Metadata = { title: "Search" };
|
|
|
|
type SearchParams = Promise<{ q?: string }>;
|
|
|
|
export default async function SearchPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: SearchParams;
|
|
}) {
|
|
const { q } = await searchParams;
|
|
const query = q?.trim();
|
|
|
|
if (!query) {
|
|
return (
|
|
<div
|
|
className="rounded-2xl border p-10 text-center"
|
|
style={{
|
|
background: "var(--color-surface)",
|
|
borderColor:
|
|
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
}}
|
|
>
|
|
<p
|
|
className="text-sm font-bold"
|
|
style={{ color: "var(--color-text-muted)" }}
|
|
>
|
|
Enter a search term to find users.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const users = await db
|
|
.select({
|
|
id: User.id,
|
|
username: User.username,
|
|
look: User.look,
|
|
motto: User.motto,
|
|
online: User.online,
|
|
rank: User.rank,
|
|
})
|
|
.from(User)
|
|
.where(like(User.username, `%${query}%`))
|
|
.limit(30)
|
|
.catch(() => []);
|
|
|
|
const rooms = await db
|
|
.select({
|
|
id: Rooms.id,
|
|
name: Rooms.name,
|
|
ownerName: Rooms.ownerName,
|
|
users: Rooms.users,
|
|
usersMax: Rooms.usersMax,
|
|
})
|
|
.from(Rooms)
|
|
.where(and(like(Rooms.name, `%${query}%`), eq(Rooms.state, "open")))
|
|
.limit(12)
|
|
.catch(() => []);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<div
|
|
className="rounded-2xl border p-4"
|
|
style={{
|
|
background: "var(--color-surface)",
|
|
borderColor:
|
|
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
}}
|
|
>
|
|
<form action="/search" method="GET" className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
name="q"
|
|
defaultValue={query}
|
|
placeholder="Search users or rooms..."
|
|
className="flex-1 rounded-xl border-2 px-4 py-2.5 text-sm font-medium"
|
|
style={{
|
|
backgroundColor: "var(--color-background)",
|
|
color: "var(--color-text-readable)",
|
|
borderColor:
|
|
"color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
|
}}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="rounded-xl px-6 py-2.5 font-extrabold text-sm transition-all duration-200 hover:scale-[1.02] active:scale-95"
|
|
style={{
|
|
background: "var(--color-primary)",
|
|
color: "var(--color-primary-foreground)",
|
|
}}
|
|
>
|
|
Search
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
|
|
Results for "{query}" — {users.length} user
|
|
{users.length !== 1 ? "s" : ""}
|
|
{rooms.length > 0 &&
|
|
`, ${rooms.length} room${rooms.length !== 1 ? "s" : ""}`}
|
|
</p>
|
|
|
|
{users.length > 0 && (
|
|
<div
|
|
className="rounded-2xl border p-5"
|
|
style={{
|
|
background: "var(--color-surface)",
|
|
borderColor:
|
|
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
}}
|
|
>
|
|
<h2
|
|
className="text-lg font-black tracking-tight mb-4"
|
|
style={{ color: "var(--color-text-readable)" }}
|
|
>
|
|
Users
|
|
</h2>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3">
|
|
{users.map((u) => (
|
|
<Link
|
|
key={u.id}
|
|
href={`/u/${u.username}`}
|
|
className="flex flex-col items-center gap-1.5 rounded-xl p-3 transition-all duration-200 hover:-translate-y-0.5"
|
|
style={{
|
|
background:
|
|
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
|
|
}}
|
|
>
|
|
<Image
|
|
src={avatarImageUrl(u.look, {
|
|
headOnly: true,
|
|
direction: 2,
|
|
})}
|
|
alt={u.username}
|
|
width={40}
|
|
height={48}
|
|
unoptimized
|
|
className="rounded-lg"
|
|
/>
|
|
<span
|
|
className="text-xs font-extrabold truncate max-w-full text-center"
|
|
style={{ color: "var(--color-text-readable)" }}
|
|
>
|
|
{u.username}
|
|
</span>
|
|
<span
|
|
className="text-[9px] font-semibold"
|
|
style={{ color: "var(--color-text-muted)" }}
|
|
>
|
|
{u.online === "1" ? "🟢 Online" : "Offline"}
|
|
</span>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{rooms.length > 0 && (
|
|
<div
|
|
className="rounded-2xl border p-5"
|
|
style={{
|
|
background: "var(--color-surface)",
|
|
borderColor:
|
|
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
}}
|
|
>
|
|
<h2
|
|
className="text-lg font-black tracking-tight mb-4"
|
|
style={{ color: "var(--color-text-readable)" }}
|
|
>
|
|
Rooms
|
|
</h2>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
{rooms.map((room) => (
|
|
<Link
|
|
key={room.id}
|
|
href={`/room/${room.id}`}
|
|
className="rounded-xl border p-3 transition-all duration-200 hover:-translate-y-0.5"
|
|
style={{
|
|
background:
|
|
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
|
|
borderColor:
|
|
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
}}
|
|
>
|
|
<p
|
|
className="text-sm font-extrabold truncate"
|
|
style={{ color: "var(--color-text-readable)" }}
|
|
>
|
|
{room.name}
|
|
</p>
|
|
<p
|
|
className="text-[10px] font-semibold mt-0.5"
|
|
style={{ color: "var(--color-text-muted)" }}
|
|
>
|
|
{room.ownerName} · {room.users}/{room.usersMax}
|
|
</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{users.length === 0 && rooms.length === 0 && (
|
|
<div
|
|
className="rounded-2xl border p-10 text-center"
|
|
style={{
|
|
background: "var(--color-surface)",
|
|
borderColor:
|
|
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
|
}}
|
|
>
|
|
<p
|
|
className="text-sm font-bold"
|
|
style={{ color: "var(--color-text-muted)" }}
|
|
>
|
|
No users or rooms found for "{query}".
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|