chore(messages): update collection translations for multiple languages

This commit is contained in:
Fatih Kadir Akın
2026-01-04 22:05:19 +03:00
parent e3844e7836
commit dcd04429b7
21 changed files with 610 additions and 49 deletions
+13
View File
@@ -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": "الأكثر تصويتاً اليوم",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+1
View File
@@ -38,6 +38,7 @@
},
"nav": {
"home": "ホーム",
"collection": "マイコレクション",
"feed": "フィード",
"promptmasters": "Promptmasters",
"prompts": "プロンプト",
+1
View File
@@ -38,6 +38,7 @@
},
"nav": {
"home": "홈",
"collection": "내 컬렉션",
"feed": "피드",
"promptmasters": "프롬프트마스터",
"prompts": "프롬프트",
+13
View File
@@ -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",
+1
View File
@@ -38,6 +38,7 @@
},
"nav": {
"home": "Главная",
"collection": "Моя Коллекция",
"feed": "Лента",
"promptmasters": "Мастера промптов",
"prompts": "Промпты",
+13
View File
@@ -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",
+1
View File
@@ -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;
+15
View File
@@ -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
+138
View File
@@ -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 });
}
}
+28
View File
@@ -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>
);
}
+102
View File
@@ -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>
);
}
+63 -36
View File
@@ -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>
+30 -13
View File
@@ -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>
);
}