Wrap next/link in a shared Link component that ships prefetch=false by default, so no route is ever prefetched (viewport or hover) anymore, and drop the DNS prefetch hint. Removes hidden background requests that were the source of intermittent issues.
193 lines
5.1 KiB
TypeScript
193 lines
5.1 KiB
TypeScript
import { and, eq, like } from "drizzle-orm";
|
|
import type { Metadata } from "next";
|
|
import Link from "@/components/link";
|
|
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
|
import { SurfaceCard } from "@/components/surface-card";
|
|
import { db, Rooms, User } from "@/lib/db";
|
|
|
|
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 (
|
|
<SurfaceCard className="p-10 text-center">
|
|
<p
|
|
className="text-sm font-bold"
|
|
style={{ color: "var(--color-text-muted)" }}
|
|
>
|
|
Enter a search term to find users.
|
|
</p>
|
|
</SurfaceCard>
|
|
);
|
|
}
|
|
|
|
const [users, rooms] = await Promise.all([
|
|
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(() => []),
|
|
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">
|
|
<SurfaceCard className="p-4">
|
|
<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>
|
|
</SurfaceCard>
|
|
|
|
<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 && (
|
|
<SurfaceCard className="p-5">
|
|
<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)",
|
|
}}
|
|
>
|
|
<UserAvatarThumbnail
|
|
figure={u.look}
|
|
alt={u.username}
|
|
options={{ direction: 2 }}
|
|
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>
|
|
</SurfaceCard>
|
|
)}
|
|
|
|
{rooms.length > 0 && (
|
|
<SurfaceCard className="p-5">
|
|
<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>
|
|
</SurfaceCard>
|
|
)}
|
|
|
|
{users.length === 0 && rooms.length === 0 && (
|
|
<SurfaceCard className="p-10 text-center">
|
|
<p
|
|
className="text-sm font-bold"
|
|
style={{ color: "var(--color-text-muted)" }}
|
|
>
|
|
No users or rooms found for "{query}".
|
|
</p>
|
|
</SurfaceCard>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|