Files
prompt/apps/web/src/app/prompts/page.tsx
dsyoon 27540269b7 Initial commit: add FastAPI MVP (모프) and existing web app
Includes FastAPI+Jinja2+HTMX+SQLite implementation with seed categories, plus deployment templates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 17:17:22 +09:00

261 lines
9.3 KiB
TypeScript

import Link from "next/link";
import { prisma } from "@/lib/prisma";
export default async function PromptsPage({
searchParams,
}: {
searchParams: Promise<{
query?: string;
entity?: string;
tag?: string;
model?: string;
sort?: "new" | "popular";
}>;
}) {
const sp = await searchParams;
const query = (sp.query ?? "").trim();
const entity = (sp.entity ?? "").trim();
const tag = (sp.tag ?? "").trim();
const model = (sp.model ?? "").trim();
const sort = sp.sort === "popular" ? "popular" : "new";
const [entities, tags, models, prompts] = await Promise.all([
prisma.entity.findMany({
where: { isPublished: true },
orderBy: { name: "asc" },
take: 100,
select: { slug: true, name: true },
}),
prisma.tag.findMany({
orderBy: { name: "asc" },
take: 200,
select: { slug: true, name: true },
}),
prisma.promptModel.findMany({
orderBy: { name: "asc" },
take: 100,
select: { slug: true, name: true, provider: true },
}),
prisma.prompt.findMany({
where: {
isPublished: true,
...(entity ? { entity: { slug: entity } } : {}),
...(model ? { model: { slug: model } } : {}),
...(tag ? { tags: { some: { tag: { slug: tag } } } } : {}),
...(query
? {
OR: [
{ title: { contains: query, mode: "insensitive" } },
{ descriptionMd: { contains: query, mode: "insensitive" } },
],
}
: {}),
},
orderBy:
sort === "popular"
? { likes: { _count: "desc" } }
: { createdAt: "desc" },
take: 50,
select: {
id: true,
title: true,
descriptionMd: true,
createdAt: true,
entity: { select: { slug: true, name: true } },
model: { select: { slug: true, name: true, provider: true } },
tags: { select: { tag: { select: { slug: true, name: true } } } },
_count: { select: { likes: true, comments: true, bookmarks: true } },
},
}),
]);
return (
<div className="min-h-screen bg-zinc-50">
<div className="mx-auto max-w-5xl px-6 py-10">
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<p className="text-sm font-medium text-zinc-500"></p>
<h1 className="text-2xl font-semibold tracking-tight">
</h1>
</div>
<Link
href="/entities"
className="text-sm font-semibold text-zinc-900 underline underline-offset-4"
>
</Link>
</div>
<form className="rounded-2xl border border-zinc-200 bg-white p-4">
<div className="grid gap-3 md:grid-cols-4">
<div className="md:col-span-2">
<label className="block text-xs font-semibold text-zinc-700">
</label>
<input
name="query"
defaultValue={query}
placeholder="예: 요약, 코드 리뷰, STAR…"
className="mt-2 h-11 w-full rounded-xl border border-zinc-200 bg-white px-4 text-sm outline-none focus:border-zinc-400"
/>
</div>
<div>
<label className="block text-xs font-semibold text-zinc-700">
(Entity)
</label>
<select
name="entity"
defaultValue={entity}
className="mt-2 h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 text-sm outline-none focus:border-zinc-400"
>
<option value=""></option>
{entities.map((e) => (
<option key={e.slug} value={e.slug}>
{e.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-zinc-700">
</label>
<select
name="sort"
defaultValue={sort}
className="mt-2 h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 text-sm outline-none focus:border-zinc-400"
>
<option value="new"></option>
<option value="popular">()</option>
</select>
</div>
</div>
<div className="mt-3 grid gap-3 md:grid-cols-3">
<div>
<label className="block text-xs font-semibold text-zinc-700">
</label>
<select
name="tag"
defaultValue={tag}
className="mt-2 h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 text-sm outline-none focus:border-zinc-400"
>
<option value=""></option>
{tags.map((t) => (
<option key={t.slug} value={t.slug}>
{t.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-zinc-700">
</label>
<select
name="model"
defaultValue={model}
className="mt-2 h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 text-sm outline-none focus:border-zinc-400"
>
<option value=""></option>
{models.map((m) => (
<option key={m.slug} value={m.slug}>
{m.name}
</option>
))}
</select>
</div>
<div className="flex items-end gap-2">
<button
type="submit"
className="h-11 w-full rounded-xl bg-zinc-900 px-4 text-sm font-semibold text-white hover:bg-zinc-800"
>
</button>
<Link
href="/prompts"
className="inline-flex h-11 shrink-0 items-center justify-center rounded-xl border border-zinc-200 bg-white px-4 text-sm font-semibold text-zinc-900 hover:bg-zinc-50"
>
</Link>
</div>
</div>
</form>
<div className="rounded-2xl border border-zinc-200 bg-white p-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold"></h2>
<p className="text-sm text-zinc-500">{prompts.length}</p>
</div>
<div className="mt-4 grid gap-4">
{prompts.map((p) => (
<Link
key={p.id}
href={`/prompts/${p.id}`}
className="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 hover:bg-white"
>
<div className="flex items-start justify-between gap-4">
<div className="flex flex-col gap-1">
<p className="text-sm font-semibold">{p.title}</p>
<p className="text-xs text-zinc-500">
{p.entity.name}
{p.model ? ` · ${p.model.name}` : ""}
</p>
</div>
<div className="text-right text-xs text-zinc-600">
<p> {p._count.likes}</p>
<p> {p._count.comments}</p>
<p> {p._count.bookmarks}</p>
</div>
</div>
{p.descriptionMd ? (
<p className="mt-2 line-clamp-2 text-sm text-zinc-700">
{p.descriptionMd}
</p>
) : null}
{p.tags.length ? (
<div className="mt-3 flex flex-wrap gap-2">
{p.tags.map((t) => (
<span
key={t.tag.slug}
className="rounded-full border border-zinc-200 bg-white px-2 py-0.5 text-xs text-zinc-700"
>
#{t.tag.name}
</span>
))}
</div>
) : null}
</Link>
))}
{!prompts.length ? (
<div className="rounded-2xl border border-zinc-200 bg-white p-6 text-sm text-zinc-600">
. .
</div>
) : null}
</div>
</div>
<div className="rounded-2xl border border-zinc-200 bg-white p-5">
<p className="text-sm font-semibold">API</p>
<p className="mt-1 text-sm text-zinc-600">
API:{" "}
<code className="rounded bg-zinc-50 px-1">/api/prompts</code>
</p>
</div>
</div>
</div>
</div>
);
}