chore(messages): update collection translations for multiple languages
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "الرئيسية",
|
||||
"collection": "مجموعتي",
|
||||
"feed": "الخلاصة",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "الأوامر",
|
||||
@@ -853,6 +854,18 @@
|
||||
"subscribeToCategories": "اشترك في التصنيفات لرؤية الأوامر هنا",
|
||||
"viewAllCategories": "عرض جميع التصنيفات"
|
||||
},
|
||||
"collection": {
|
||||
"title": "مجموعتي",
|
||||
"description": "الأوامر التي حفظتها لوقت لاحق",
|
||||
"browsePrompts": "تصفح الأوامر",
|
||||
"discover": "اكتشف",
|
||||
"emptyTitle": "مجموعتك فارغة",
|
||||
"emptyDescription": "احفظ الأوامر في مجموعتك للوصول إليها بسرعة",
|
||||
"addToCollection": "أضف للمجموعة",
|
||||
"inCollection": "في المجموعة",
|
||||
"added": "تمت الإضافة للمجموعة",
|
||||
"removed": "تمت الإزالة من المجموعة"
|
||||
},
|
||||
"discovery": {
|
||||
"featuredPrompts": "الأوامر المميزة",
|
||||
"todaysMostUpvoted": "الأكثر تصويتاً اليوم",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "Ana səhifə",
|
||||
"collection": "Kolleksiyam",
|
||||
"feed": "Lent",
|
||||
"promptmasters": "Promptmasterlər",
|
||||
"prompts": "Promptlar",
|
||||
@@ -853,6 +854,18 @@
|
||||
"subscribeToCategories": "Burada promptları görmək üçün kateqoriyalara abunə olun",
|
||||
"viewAllCategories": "Bütün Kateqoriyalara Bax"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Kolleksiyam",
|
||||
"description": "Sonra üçün saxladığınız promptlar",
|
||||
"browsePrompts": "Promptlara Bax",
|
||||
"discover": "Kəşf et",
|
||||
"emptyTitle": "Kolleksiyanız boşdur",
|
||||
"emptyDescription": "Tez daxil olmaq üçün promptları kolleksiyanıza saxlayın",
|
||||
"addToCollection": "Kolleksiyaya Əlavə Et",
|
||||
"inCollection": "Kolleksiyada",
|
||||
"added": "Kolleksiyaya əlavə edildi",
|
||||
"removed": "Kolleksiyadan silindi"
|
||||
},
|
||||
"discovery": {
|
||||
"featuredPrompts": "Seçilmiş Promptlar",
|
||||
"todaysMostUpvoted": "Bu Günün Ən Çox Bəyənilənləri",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "Startseite",
|
||||
"collection": "Meine Sammlung",
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
@@ -853,6 +854,18 @@
|
||||
"subscribeToCategories": "Abonnieren Sie Kategorien, um hier Prompts zu sehen",
|
||||
"viewAllCategories": "Alle Kategorien anzeigen"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Meine Sammlung",
|
||||
"description": "Prompts, die Sie für später gespeichert haben",
|
||||
"browsePrompts": "Prompts durchsuchen",
|
||||
"discover": "Entdecken",
|
||||
"emptyTitle": "Ihre Sammlung ist leer",
|
||||
"emptyDescription": "Speichern Sie Prompts in Ihrer Sammlung für schnellen Zugriff",
|
||||
"addToCollection": "Zur Sammlung hinzufügen",
|
||||
"inCollection": "In Sammlung",
|
||||
"added": "Zur Sammlung hinzugefügt",
|
||||
"removed": "Aus Sammlung entfernt"
|
||||
},
|
||||
"discovery": {
|
||||
"featuredPrompts": "Hervorgehobene Prompts",
|
||||
"todaysMostUpvoted": "Heute am meisten bewertet",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"collection": "My Collection",
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
@@ -853,6 +854,18 @@
|
||||
"subscribeToCategories": "Subscribe to categories to see prompts here",
|
||||
"viewAllCategories": "View All Categories"
|
||||
},
|
||||
"collection": {
|
||||
"title": "My Collection",
|
||||
"description": "Prompts you've saved for later",
|
||||
"browsePrompts": "Browse Prompts",
|
||||
"discover": "Discover",
|
||||
"emptyTitle": "Your collection is empty",
|
||||
"emptyDescription": "Save prompts to your collection to access them quickly",
|
||||
"addToCollection": "Add to Collection",
|
||||
"inCollection": "In Collection",
|
||||
"added": "Added to collection",
|
||||
"removed": "Removed from collection"
|
||||
},
|
||||
"discovery": {
|
||||
"featuredPrompts": "Featured Prompts",
|
||||
"todaysMostUpvoted": "Today's Most Upvoted",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "Inicio",
|
||||
"collection": "Mi Colección",
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
@@ -853,6 +854,18 @@
|
||||
"subscribeToCategories": "Suscríbete a categorías para ver prompts aquí",
|
||||
"viewAllCategories": "Ver Todas las Categorías"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Mi Colección",
|
||||
"description": "Prompts que has guardado para después",
|
||||
"browsePrompts": "Ver Prompts",
|
||||
"discover": "Descubrir",
|
||||
"emptyTitle": "Tu colección está vacía",
|
||||
"emptyDescription": "Guarda prompts en tu colección para acceder rápidamente",
|
||||
"addToCollection": "Añadir a Colección",
|
||||
"inCollection": "En Colección",
|
||||
"added": "Añadido a la colección",
|
||||
"removed": "Eliminado de la colección"
|
||||
},
|
||||
"discovery": {
|
||||
"featuredPrompts": "Prompts Destacados",
|
||||
"todaysMostUpvoted": "Más Votados Hoy",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "Accueil",
|
||||
"collection": "Ma Collection",
|
||||
"feed": "Flux",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
@@ -853,6 +854,18 @@
|
||||
"subscribeToCategories": "Abonnez-vous à des catégories pour voir des prompts ici",
|
||||
"viewAllCategories": "Voir Toutes les Catégories"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Ma Collection",
|
||||
"description": "Prompts que vous avez sauvegardés pour plus tard",
|
||||
"browsePrompts": "Parcourir les Prompts",
|
||||
"discover": "Découvrir",
|
||||
"emptyTitle": "Votre collection est vide",
|
||||
"emptyDescription": "Sauvegardez des prompts dans votre collection pour y accéder rapidement",
|
||||
"addToCollection": "Ajouter à la Collection",
|
||||
"inCollection": "Dans la Collection",
|
||||
"added": "Ajouté à la collection",
|
||||
"removed": "Retiré de la collection"
|
||||
},
|
||||
"discovery": {
|
||||
"featuredPrompts": "Prompts en Vedette",
|
||||
"todaysMostUpvoted": "Les Plus Votés Aujourd'hui",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"collection": "La Mia Collezione",
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmaster",
|
||||
"prompts": "Prompt",
|
||||
@@ -853,6 +854,18 @@
|
||||
"subscribeToCategories": "Iscriviti alle categorie per vedere i prompt qui",
|
||||
"viewAllCategories": "Vedi Tutte le Categorie"
|
||||
},
|
||||
"collection": {
|
||||
"title": "La Mia Collezione",
|
||||
"description": "Prompt che hai salvato per dopo",
|
||||
"browsePrompts": "Sfoglia Prompt",
|
||||
"discover": "Scopri",
|
||||
"emptyTitle": "La tua collezione è vuota",
|
||||
"emptyDescription": "Salva i prompt nella tua collezione per accedervi rapidamente",
|
||||
"addToCollection": "Aggiungi alla Collezione",
|
||||
"inCollection": "Nella Collezione",
|
||||
"added": "Aggiunto alla collezione",
|
||||
"removed": "Rimosso dalla collezione"
|
||||
},
|
||||
"discovery": {
|
||||
"featuredPrompts": "Prompt in Evidenza",
|
||||
"todaysMostUpvoted": "I Più Votati di Oggi",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "ホーム",
|
||||
"collection": "マイコレクション",
|
||||
"feed": "フィード",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "プロンプト",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "홈",
|
||||
"collection": "내 컬렉션",
|
||||
"feed": "피드",
|
||||
"promptmasters": "프롬프트마스터",
|
||||
"prompts": "프롬프트",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "Início",
|
||||
"collection": "Minha Coleção",
|
||||
"feed": "Feed",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Prompts",
|
||||
@@ -853,6 +854,18 @@
|
||||
"subscribeToCategories": "Inscreva-se em categorias para ver prompts aqui",
|
||||
"viewAllCategories": "Ver Todas as Categorias"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Minha Coleção",
|
||||
"description": "Prompts que você salvou para depois",
|
||||
"browsePrompts": "Ver Prompts",
|
||||
"discover": "Descobrir",
|
||||
"emptyTitle": "Sua coleção está vazia",
|
||||
"emptyDescription": "Salve prompts na sua coleção para acessá-los rapidamente",
|
||||
"addToCollection": "Adicionar à Coleção",
|
||||
"inCollection": "Na Coleção",
|
||||
"added": "Adicionado à coleção",
|
||||
"removed": "Removido da coleção"
|
||||
},
|
||||
"discovery": {
|
||||
"featuredPrompts": "Prompts em Destaque",
|
||||
"todaysMostUpvoted": "Mais Votados de Hoje",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "Главная",
|
||||
"collection": "Моя Коллекция",
|
||||
"feed": "Лента",
|
||||
"promptmasters": "Мастера промптов",
|
||||
"prompts": "Промпты",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "Ana Sayfa",
|
||||
"collection": "Koleksiyonum",
|
||||
"feed": "Akış",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "Promptlar",
|
||||
@@ -853,6 +854,18 @@
|
||||
"subscribeToCategories": "Burada promptları görmek için kategorilere abone olun",
|
||||
"viewAllCategories": "Tüm Kategorileri Gör"
|
||||
},
|
||||
"collection": {
|
||||
"title": "Koleksiyonum",
|
||||
"description": "Sonra için kaydettiğiniz promptlar",
|
||||
"browsePrompts": "Promptlara Göz At",
|
||||
"discover": "Keşfet",
|
||||
"emptyTitle": "Koleksiyonunuz boş",
|
||||
"emptyDescription": "Hızlı erişim için promptları koleksiyonunuza kaydedin",
|
||||
"addToCollection": "Koleksiyona Ekle",
|
||||
"inCollection": "Koleksiyonda",
|
||||
"added": "Koleksiyona eklendi",
|
||||
"removed": "Koleksiyondan kaldırıldı"
|
||||
},
|
||||
"discovery": {
|
||||
"featuredPrompts": "Öne Çıkan Promptlar",
|
||||
"todaysMostUpvoted": "Bugün En Çok Oy Alanlar",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"home": "首页",
|
||||
"collection": "我的收藏",
|
||||
"feed": "动态",
|
||||
"promptmasters": "Promptmasters",
|
||||
"prompts": "提示词",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "collections" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"promptId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "collections_pkey" PRIMARY KEY ("userId","promptId")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "collections_userId_idx" ON "collections"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "collections_promptId_idx" ON "collections"("promptId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "collections" ADD CONSTRAINT "collections_promptId_fkey" FOREIGN KEY ("promptId") REFERENCES "prompts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "collections" ADD CONSTRAINT "collections_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -43,6 +43,7 @@ model User {
|
||||
commentVotes CommentVote[]
|
||||
notifications Notification[] @relation("NotificationRecipient")
|
||||
notificationsActed Notification[] @relation("NotificationActor")
|
||||
collections Collection[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -122,6 +123,7 @@ model Prompt {
|
||||
contributors User[] @relation("PromptContributors")
|
||||
outgoingConnections PromptConnection[] @relation("ConnectionSource")
|
||||
incomingConnections PromptConnection[] @relation("ConnectionTarget")
|
||||
collectedBy Collection[]
|
||||
|
||||
@@index([authorId])
|
||||
@@index([categoryId])
|
||||
@@ -248,6 +250,19 @@ model PinnedPrompt {
|
||||
@@map("pinned_prompts")
|
||||
}
|
||||
|
||||
model Collection {
|
||||
userId String
|
||||
promptId String
|
||||
createdAt DateTime @default(now())
|
||||
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([userId, promptId])
|
||||
@@index([userId])
|
||||
@@index([promptId])
|
||||
@@map("collections")
|
||||
}
|
||||
|
||||
model PromptReport {
|
||||
id String @id @default(cuid())
|
||||
reason ReportReason
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { z } from "zod";
|
||||
|
||||
const addToCollectionSchema = z.object({
|
||||
promptId: z.string().min(1),
|
||||
});
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const collections = await db.collection.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: {
|
||||
prompt: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
verified: true,
|
||||
},
|
||||
},
|
||||
category: {
|
||||
include: {
|
||||
parent: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: { votes: true, contributors: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ collections });
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { promptId } = addToCollectionSchema.parse(body);
|
||||
|
||||
const existingCollection = await db.collection.findUnique({
|
||||
where: {
|
||||
userId_promptId: {
|
||||
userId: session.user.id,
|
||||
promptId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCollection) {
|
||||
return NextResponse.json({ error: "Already in collection" }, { status: 400 });
|
||||
}
|
||||
|
||||
const prompt = await db.prompt.findUnique({
|
||||
where: { id: promptId },
|
||||
select: { id: true, isPrivate: true, authorId: true },
|
||||
});
|
||||
|
||||
if (!prompt) {
|
||||
return NextResponse.json({ error: "Prompt not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (prompt.isPrivate && prompt.authorId !== session.user.id) {
|
||||
return NextResponse.json({ error: "Cannot add private prompt" }, { status: 403 });
|
||||
}
|
||||
|
||||
const collection = await db.collection.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
promptId,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ collection, added: true });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
|
||||
}
|
||||
console.error("Failed to add to collection:", error);
|
||||
return NextResponse.json({ error: "Failed to add to collection" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const promptId = searchParams.get("promptId");
|
||||
|
||||
if (!promptId) {
|
||||
return NextResponse.json({ error: "promptId required" }, { status: 400 });
|
||||
}
|
||||
|
||||
await db.collection.delete({
|
||||
where: {
|
||||
userId_promptId: {
|
||||
userId: session.user.id,
|
||||
promptId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ removed: true });
|
||||
} catch (error) {
|
||||
console.error("Failed to remove from collection:", error);
|
||||
return NextResponse.json({ error: "Failed to remove from collection" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function CollectionLoading() {
|
||||
return (
|
||||
<div className="container py-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="h-9 w-28" />
|
||||
</div>
|
||||
|
||||
{/* Collection Items */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="border rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-6 w-6 rounded-full" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
<Skeleton className="h-5 w-3/4" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { ArrowRight, Bookmark, Sparkles } from "lucide-react";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PromptList } from "@/components/prompts/prompt-list";
|
||||
|
||||
export default async function CollectionPage() {
|
||||
const t = await getTranslations("collection");
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const collectionsRaw = await db.collection.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: {
|
||||
prompt: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
verified: true,
|
||||
},
|
||||
},
|
||||
category: {
|
||||
include: {
|
||||
parent: {
|
||||
select: { id: true, name: true, slug: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: { votes: true, contributors: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const prompts = collectionsRaw
|
||||
.filter((c) => c.prompt && !c.prompt.deletedAt)
|
||||
.map((c) => ({
|
||||
...c.prompt,
|
||||
voteCount: c.prompt._count?.votes ?? 0,
|
||||
contributorCount: c.prompt._count?.contributors ?? 0,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="container py-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">{t("title")}</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/prompts">
|
||||
{t("browsePrompts")}
|
||||
<ArrowRight className="ml-1.5 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/discover">
|
||||
<Sparkles className="mr-1.5 h-4 w-4" />
|
||||
{t("discover")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{prompts.length > 0 ? (
|
||||
<PromptList prompts={prompts} currentPage={1} totalPages={1} />
|
||||
) : (
|
||||
<div className="text-center py-12 border rounded-lg bg-muted/30">
|
||||
<Bookmark className="h-10 w-10 text-muted-foreground mx-auto mb-3" />
|
||||
<h2 className="font-medium mb-1">{t("emptyTitle")}</h2>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t("emptyDescription")}
|
||||
</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/prompts">{t("browsePrompts")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { RestorePromptButton } from "@/components/prompts/restore-prompt-button"
|
||||
import { CommentSection } from "@/components/comments";
|
||||
import { PromptFlowSection } from "@/components/prompts/prompt-flow-section";
|
||||
import { RelatedPrompts } from "@/components/prompts/related-prompts";
|
||||
import { AddToCollectionButton } from "@/components/prompts/add-to-collection-button";
|
||||
import { getConfig } from "@/lib/config";
|
||||
import { StructuredData } from "@/components/seo/structured-data";
|
||||
|
||||
@@ -146,6 +147,18 @@ export default async function PromptPage({ params }: PromptPageProps) {
|
||||
})
|
||||
: null;
|
||||
|
||||
// Check if user has this prompt in their collection
|
||||
const userCollection = session?.user
|
||||
? await db.collection.findUnique({
|
||||
where: {
|
||||
userId_promptId: {
|
||||
userId: session.user.id,
|
||||
promptId: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
// Fetch related prompts (via PromptConnection with label "related")
|
||||
const relatedConnections = await db.promptConnection.findMany({
|
||||
where: {
|
||||
@@ -208,6 +221,7 @@ export default async function PromptPage({ params }: PromptPageProps) {
|
||||
const canEdit = isOwner || isAdmin;
|
||||
const voteCount = prompt._count?.votes ?? 0;
|
||||
const hasVoted = !!userVote;
|
||||
const inCollection = !!userCollection;
|
||||
|
||||
// Fetch change requests for this prompt
|
||||
const changeRequests = await db.changeRequest.findMany({
|
||||
@@ -327,23 +341,6 @@ export default async function PromptPage({ params }: PromptPageProps) {
|
||||
<Badge variant="secondary">{t("promptPrivate")}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{isOwner && (
|
||||
<>
|
||||
<Button variant="outline" size="icon" asChild className="sm:hidden">
|
||||
<Link href={`/prompts/${id}/edit`}>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild className="hidden sm:flex">
|
||||
<Link href={`/prompts/${id}/edit`}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
{t("edit")}
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -445,17 +442,32 @@ export default async function PromptPage({ params }: PromptPageProps) {
|
||||
{/* Content Tabs */}
|
||||
<Tabs defaultValue="content">
|
||||
<div className="flex flex-col gap-3 mb-4">
|
||||
{/* Propose changes button - on top on mobile */}
|
||||
{!isOwner && session?.user && (
|
||||
<div className="md:hidden">
|
||||
<Button asChild size="sm" className="w-full">
|
||||
<Link href={`/prompts/${id}/changes/new`}>
|
||||
<GitPullRequest className="h-4 w-4 mr-1.5" />
|
||||
{t("createChangeRequest")}
|
||||
</Link>
|
||||
</Button>
|
||||
{/* Action buttons - on top on mobile */}
|
||||
<div className="flex items-center justify-between gap-2 md:hidden">
|
||||
<AddToCollectionButton
|
||||
promptId={prompt.id}
|
||||
initialInCollection={inCollection}
|
||||
isLoggedIn={!!session?.user}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
{!isOwner && session?.user && (
|
||||
<Button asChild size="sm">
|
||||
<Link href={`/prompts/${id}/changes/new`}>
|
||||
<GitPullRequest className="h-4 w-4 mr-1.5" />
|
||||
{t("createChangeRequest")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{isOwner && (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/prompts/${id}/edit`}>
|
||||
<Edit className="h-4 w-4 mr-1.5" />
|
||||
{t("edit")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<TabsList>
|
||||
<TabsTrigger value="content">{t("promptContent")}</TabsTrigger>
|
||||
@@ -478,15 +490,30 @@ export default async function PromptPage({ params }: PromptPageProps) {
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
{/* Propose changes button - inline on desktop */}
|
||||
{!isOwner && session?.user && (
|
||||
<Button asChild size="sm" className="hidden md:inline-flex">
|
||||
<Link href={`/prompts/${id}/changes/new`}>
|
||||
<GitPullRequest className="h-4 w-4 mr-1.5" />
|
||||
{t("createChangeRequest")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{/* Action buttons - inline on desktop */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<AddToCollectionButton
|
||||
promptId={prompt.id}
|
||||
initialInCollection={inCollection}
|
||||
isLoggedIn={!!session?.user}
|
||||
/>
|
||||
{!isOwner && session?.user && (
|
||||
<Button asChild size="sm">
|
||||
<Link href={`/prompts/${id}/changes/new`}>
|
||||
<GitPullRequest className="h-4 w-4 mr-1.5" />
|
||||
{t("createChangeRequest")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{isOwner && (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/prompts/${id}/edit`}>
|
||||
<Edit className="h-4 w-4 mr-1.5" />
|
||||
{t("edit")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -142,13 +142,22 @@ export function Header({ authProvider = "credentials", allowRegistration = true
|
||||
<nav className="flex-1 p-4">
|
||||
<div className="space-y-1">
|
||||
{user && (
|
||||
<Link
|
||||
href="/feed"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||
>
|
||||
{t("nav.feed")}
|
||||
</Link>
|
||||
<>
|
||||
<Link
|
||||
href="/collection"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||
>
|
||||
{t("nav.collection")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/feed"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||
>
|
||||
{t("nav.feed")}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href="/prompts"
|
||||
@@ -262,12 +271,20 @@ export function Header({ authProvider = "credentials", allowRegistration = true
|
||||
{/* Desktop nav */}
|
||||
<nav className="hidden md:flex items-center gap-1 text-sm">
|
||||
{user && (
|
||||
<Link
|
||||
href="/feed"
|
||||
className="px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
>
|
||||
{t("nav.feed")}
|
||||
</Link>
|
||||
<>
|
||||
<Link
|
||||
href="/collection"
|
||||
className="px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
>
|
||||
{t("nav.collection")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/feed"
|
||||
className="px-3 py-1.5 rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
|
||||
>
|
||||
{t("nav.feed")}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href="/prompts"
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Bookmark, Check, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AddToCollectionButtonProps {
|
||||
promptId: string;
|
||||
initialInCollection?: boolean;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export function AddToCollectionButton({
|
||||
promptId,
|
||||
initialInCollection = false,
|
||||
isLoggedIn,
|
||||
}: AddToCollectionButtonProps) {
|
||||
const t = useTranslations("collection");
|
||||
const [inCollection, setInCollection] = useState(initialInCollection);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!isLoggedIn) {
|
||||
window.location.href = "/login";
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
if (inCollection) {
|
||||
const res = await fetch(`/api/collection?promptId=${promptId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setInCollection(false);
|
||||
setShowTooltip(true);
|
||||
setTimeout(() => setShowTooltip(false), 2000);
|
||||
}
|
||||
} else {
|
||||
const res = await fetch("/api/collection", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ promptId }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setInCollection(true);
|
||||
setShowTooltip(true);
|
||||
setTimeout(() => setShowTooltip(false), 2000);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update collection:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant={inCollection ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
onClick={handleClick}
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
"gap-1.5",
|
||||
inCollection && "bg-primary/10 text-primary hover:bg-primary/20"
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : inCollection ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Bookmark className="h-4 w-4" />
|
||||
)}
|
||||
{inCollection ? t("inCollection") : t("addToCollection")}
|
||||
</Button>
|
||||
|
||||
{showTooltip && (
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-2 py-1 text-xs font-medium bg-foreground text-background rounded shadow-lg whitespace-nowrap z-50 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
{inCollection ? t("added") : t("removed")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user