chore(api): sync prompts.csv from API and update message translations

This commit is contained in:
Fatih Kadir Akın
2025-12-13 23:37:43 +03:00
parent f71a5a3560
commit b6c911a766
21 changed files with 404 additions and 263 deletions
+20
View File
@@ -34,6 +34,26 @@ jobs:
chmod +x scripts/generate-contributors.sh chmod +x scripts/generate-contributors.sh
./scripts/generate-contributors.sh ./scripts/generate-contributors.sh
- name: Sync prompts.csv from API (removes unlisted prompts)
env:
PROMPTS_API_URL: "https://prompts.chat"
run: |
if [ -n "$PROMPTS_API_URL" ]; then
echo "Fetching prompts.csv from API..."
curl -s "${PROMPTS_API_URL}/prompts.csv" -o prompts.csv.new
if [ -s prompts.csv.new ]; then
mv prompts.csv.new prompts.csv
git add prompts.csv
git diff --cached --quiet || git commit -m "Update prompts.csv (sync from API)"
echo "prompts.csv updated from API"
else
echo "Warning: Empty response from API, keeping existing prompts.csv"
rm -f prompts.csv.new
fi
else
echo "PROMPTS_API_URL not set, skipping prompts.csv sync"
fi
- name: Push changes - name: Push changes
run: | run: |
git push origin main --force-with-lease git push origin main --force-with-lease
+2
View File
@@ -103,6 +103,8 @@
"promptPublic": "عام", "promptPublic": "عام",
"feature": "تمييز", "feature": "تمييز",
"featured": "مميز", "featured": "مميز",
"unlist": "إلغاء الإدراج",
"relist": "إعادة الإدراج",
"adminArea": "منطقة المسؤول", "adminArea": "منطقة المسؤول",
"types": { "types": {
"text": "نص", "text": "نص",
+2
View File
@@ -103,6 +103,8 @@
"promptPublic": "Öffentlich", "promptPublic": "Öffentlich",
"feature": "Hervorheben", "feature": "Hervorheben",
"featured": "Hervorgehoben", "featured": "Hervorgehoben",
"unlist": "Ausblenden",
"relist": "Wieder einblenden",
"adminArea": "Admin-Bereich", "adminArea": "Admin-Bereich",
"types": { "types": {
"text": "Text", "text": "Text",
+2
View File
@@ -103,6 +103,8 @@
"promptPublic": "Public", "promptPublic": "Public",
"feature": "Feature", "feature": "Feature",
"featured": "Featured", "featured": "Featured",
"unlist": "Unlist",
"relist": "Relist",
"adminArea": "Admin Area", "adminArea": "Admin Area",
"types": { "types": {
"text": "Text", "text": "Text",
+2
View File
@@ -101,6 +101,8 @@
"promptPublic": "Público", "promptPublic": "Público",
"feature": "Destacar", "feature": "Destacar",
"featured": "Destacado", "featured": "Destacado",
"unlist": "Ocultar",
"relist": "Mostrar",
"adminArea": "Área de Administrador", "adminArea": "Área de Administrador",
"noMorePrompts": "Has llegado al final", "noMorePrompts": "Has llegado al final",
"loading": "Cargando...", "loading": "Cargando...",
+2
View File
@@ -103,6 +103,8 @@
"promptPublic": "Public", "promptPublic": "Public",
"feature": "Mettre en avant", "feature": "Mettre en avant",
"featured": "En vedette", "featured": "En vedette",
"unlist": "Retirer",
"relist": "Remettre",
"adminArea": "Zone Admin", "adminArea": "Zone Admin",
"types": { "types": {
"text": "Texte", "text": "Texte",
+2
View File
@@ -103,6 +103,8 @@
"promptPublic": "Pubblico", "promptPublic": "Pubblico",
"feature": "In evidenza", "feature": "In evidenza",
"featured": "In evidenza", "featured": "In evidenza",
"unlist": "Nascondi",
"relist": "Mostra",
"adminArea": "Area Admin", "adminArea": "Area Admin",
"types": { "types": {
"text": "Testo", "text": "Testo",
+2
View File
@@ -101,6 +101,8 @@
"promptPublic": "公開", "promptPublic": "公開",
"feature": "注目に設定", "feature": "注目に設定",
"featured": "注目", "featured": "注目",
"unlist": "非公開にする",
"relist": "公開に戻す",
"adminArea": "管理者エリア", "adminArea": "管理者エリア",
"noMorePrompts": "最後まで表示しました", "noMorePrompts": "最後まで表示しました",
"loading": "読み込み中...", "loading": "読み込み中...",
+2
View File
@@ -103,6 +103,8 @@
"promptPublic": "공개", "promptPublic": "공개",
"feature": "추천", "feature": "추천",
"featured": "추천됨", "featured": "추천됨",
"unlist": "숨기기",
"relist": "다시 표시",
"adminArea": "관리자 영역", "adminArea": "관리자 영역",
"types": { "types": {
"text": "텍스트", "text": "텍스트",
+2
View File
@@ -103,6 +103,8 @@
"promptPublic": "Público", "promptPublic": "Público",
"feature": "Destacar", "feature": "Destacar",
"featured": "Destacado", "featured": "Destacado",
"unlist": "Ocultar",
"relist": "Mostrar",
"adminArea": "Área Admin", "adminArea": "Área Admin",
"types": { "types": {
"text": "Texto", "text": "Texto",
+2
View File
@@ -103,6 +103,8 @@
"promptPublic": "Herkese Açık", "promptPublic": "Herkese Açık",
"feature": "Öne Çıkar", "feature": "Öne Çıkar",
"featured": "Öne Çıkan", "featured": "Öne Çıkan",
"unlist": "Listeden Kaldır",
"relist": "Listeye Ekle",
"adminArea": "Yönetici Alanı", "adminArea": "Yönetici Alanı",
"types": { "types": {
"text": "Metin", "text": "Metin",
+2
View File
@@ -101,6 +101,8 @@
"promptPublic": "公开", "promptPublic": "公开",
"feature": "精选", "feature": "精选",
"featured": "已精选", "featured": "已精选",
"unlist": "取消列出",
"relist": "重新列出",
"adminArea": "管理员区域", "adminArea": "管理员区域",
"noMorePrompts": "已到达末尾", "noMorePrompts": "已到达末尾",
"loading": "加载中...", "loading": "加载中...",
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "prisma generate && next build", "build": "prisma generate && prisma migrate deploy && next build",
"start": "next start", "start": "next start",
"lint": "eslint", "lint": "eslint",
"db:generate": "prisma generate", "db:generate": "prisma generate",
@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "prompts" ADD COLUMN IF NOT EXISTS "isUnlisted" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "prompts" ADD COLUMN IF NOT EXISTS "unlistedAt" TIMESTAMP(3);
-- CreateIndex
CREATE INDEX IF NOT EXISTS "prompts_isUnlisted_idx" ON "prompts"("isUnlisted");
+213 -262
View File
@@ -1,6 +1,3 @@
// Prisma schema for prompts.chat
// https://pris.ly/d/prisma-schema
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
} }
@@ -10,41 +7,30 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
// ============================================
// User & Authentication
// ============================================
enum UserRole {
ADMIN
USER
}
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
email String @unique email String @unique
username String @unique username String @unique
name String? name String?
password String? // For credentials auth password String?
avatar String? avatar String?
role UserRole @default(USER) role UserRole @default(USER)
locale String @default("en") locale String @default("en")
verified Boolean @default(false)
emailVerified DateTime? emailVerified DateTime?
githubUsername String? // Immutable GitHub username for contributor attribution createdAt DateTime @default(now())
createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
updatedAt DateTime @updatedAt verified Boolean @default(false)
githubUsername String?
// Relations
prompts Prompt[] @relation("PromptAuthor")
contributions Prompt[] @relation("PromptContributors")
promptVersions PromptVersion[]
changeRequests ChangeRequest[]
accounts Account[] accounts Account[]
sessions Session[]
subscriptions CategorySubscription[] subscriptions CategorySubscription[]
votes PromptVote[] changeRequests ChangeRequest[]
pinnedPrompts PinnedPrompt[] pinnedPrompts PinnedPrompt[]
reports PromptReport[] reports PromptReport[]
promptVersions PromptVersion[]
votes PromptVote[]
prompts Prompt[] @relation("PromptAuthor")
sessions Session[]
contributions Prompt[] @relation("PromptContributors")
@@map("users") @@map("users")
} }
@@ -55,15 +41,14 @@ model Account {
type String type String
provider String provider String
providerAccountId String providerAccountId String
refresh_token String? @db.Text refresh_token String?
access_token String? @db.Text access_token String?
expires_at Int? expires_at Int?
token_type String? token_type String?
scope String? scope String?
id_token String? @db.Text id_token String?
session_state String? session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId]) @@unique([provider, providerAccountId])
@@map("accounts") @@map("accounts")
@@ -88,9 +73,199 @@ model VerificationToken {
@@map("verification_tokens") @@map("verification_tokens")
} }
// ============================================ model Prompt {
// Prompts & Content id String @id @default(cuid())
// ============================================ title String
description String?
content String
type PromptType @default(TEXT)
isPrivate Boolean @default(false)
mediaUrl String?
viewCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId String
categoryId String?
embedding Json?
requiredMediaCount Int?
requiredMediaType RequiredMediaType?
requiresMediaUpload Boolean @default(false)
structuredFormat StructuredFormat?
featuredAt DateTime?
isFeatured Boolean @default(false)
isUnlisted Boolean @default(false)
unlistedAt DateTime?
deletedAt DateTime?
changeRequests ChangeRequest[]
pinnedBy PinnedPrompt[]
reports PromptReport[]
tags PromptTag[]
versions PromptVersion[]
votes PromptVote[]
author User @relation("PromptAuthor", fields: [authorId], references: [id], onDelete: Cascade)
category Category? @relation(fields: [categoryId], references: [id])
contributors User[] @relation("PromptContributors")
@@index([authorId])
@@index([categoryId])
@@index([type])
@@index([isPrivate])
@@index([isFeatured])
@@index([isUnlisted])
@@map("prompts")
}
model PromptVersion {
id String @id @default(cuid())
version Int
content String
changeNote String?
createdAt DateTime @default(now())
promptId String
createdBy String
author User @relation(fields: [createdBy], references: [id], onDelete: Cascade)
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
@@unique([promptId, version])
@@index([promptId])
@@map("prompt_versions")
}
model ChangeRequest {
id String @id @default(cuid())
proposedContent String
proposedTitle String?
reason String?
status ChangeRequestStatus @default(PENDING)
reviewNote String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
promptId String
authorId String
originalContent String
originalTitle String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
@@index([promptId])
@@index([authorId])
@@index([status])
@@map("change_requests")
}
model Category {
id String @id @default(cuid())
name String
slug String @unique
description String?
icon String?
order Int @default(0)
parentId String?
parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id])
children Category[] @relation("CategoryHierarchy")
subscribers CategorySubscription[]
prompts Prompt[]
@@index([parentId])
@@map("categories")
}
model Tag {
id String @id @default(cuid())
name String @unique
slug String @unique
color String @default("#6366f1")
prompts PromptTag[]
@@map("tags")
}
model PromptTag {
promptId String
tagId String
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([promptId, tagId])
@@map("prompt_tags")
}
model CategorySubscription {
userId String
categoryId String
createdAt DateTime @default(now())
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([userId, categoryId])
@@index([userId])
@@index([categoryId])
@@map("category_subscriptions")
}
model PromptVote {
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("prompt_votes")
}
model PinnedPrompt {
userId String
promptId String
order Int @default(0)
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])
@@map("pinned_prompts")
}
model PromptReport {
id String @id @default(cuid())
reason ReportReason
details String?
status ReportStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
promptId String
reporterId String
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
reporter User @relation(fields: [reporterId], references: [id], onDelete: Cascade)
@@index([promptId])
@@index([reporterId])
@@index([status])
@@map("prompt_reports")
}
model WebhookConfig {
id String @id @default(cuid())
name String
url String
method String @default("POST")
headers Json?
payload String
events WebhookEvent[]
isEnabled Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("webhook_configs")
}
enum UserRole {
ADMIN
USER
}
enum PromptType { enum PromptType {
TEXT TEXT
@@ -117,191 +292,6 @@ enum RequiredMediaType {
DOCUMENT DOCUMENT
} }
model Prompt {
id String @id @default(cuid())
title String
description String?
content String @db.Text
type PromptType @default(TEXT)
structuredFormat StructuredFormat? // For STRUCTURED type: JSON or YAML
isPrivate Boolean @default(false)
mediaUrl String? // For non-text prompts
// AI Search embedding (stored as JSON array of floats)
embedding Json? // OpenAI embedding vector for semantic search
// Media requirements (user needs to upload media to use this prompt)
requiresMediaUpload Boolean @default(false)
requiredMediaType RequiredMediaType?
requiredMediaCount Int?
// Metadata
viewCount Int @default(0)
isFeatured Boolean @default(false)
featuredAt DateTime?
deletedAt DateTime? // Soft delete - CC0 prompts cannot be permanently deleted
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
authorId String
author User @relation("PromptAuthor", fields: [authorId], references: [id], onDelete: Cascade)
categoryId String?
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
versions PromptVersion[]
changeRequests ChangeRequest[]
tags PromptTag[]
votes PromptVote[]
contributors User[] @relation("PromptContributors")
pinnedBy PinnedPrompt[]
reports PromptReport[]
@@index([authorId])
@@index([categoryId])
@@index([type])
@@index([isPrivate])
@@index([isFeatured])
@@map("prompts")
}
model PromptVersion {
id String @id @default(cuid())
version Int
content String @db.Text
changeNote String?
createdAt DateTime @default(now())
// Relations
promptId String
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
createdBy String
author User @relation(fields: [createdBy], references: [id], onDelete: Cascade)
@@unique([promptId, version])
@@index([promptId])
@@map("prompt_versions")
}
model ChangeRequest {
id String @id @default(cuid())
originalContent String @db.Text
originalTitle String
proposedContent String @db.Text
proposedTitle String?
reason String?
status ChangeRequestStatus @default(PENDING)
reviewNote String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
promptId String
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
@@index([promptId])
@@index([authorId])
@@index([status])
@@map("change_requests")
}
// ============================================
// Categories & Tags
// ============================================
model Category {
id String @id @default(cuid())
name String
slug String @unique
description String?
icon String?
order Int @default(0)
// Self-referential for hierarchy
parentId String?
parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id], onDelete: SetNull)
children Category[] @relation("CategoryHierarchy")
prompts Prompt[]
subscribers CategorySubscription[]
@@index([parentId])
@@map("categories")
}
model Tag {
id String @id @default(cuid())
name String @unique
slug String @unique
color String @default("#6366f1")
prompts PromptTag[]
@@map("tags")
}
model PromptTag {
promptId String
tagId String
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([promptId, tagId])
@@map("prompt_tags")
}
// ============================================
// Subscriptions
// ============================================
model CategorySubscription {
userId String
categoryId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
@@id([userId, categoryId])
@@index([userId])
@@index([categoryId])
@@map("category_subscriptions")
}
model PromptVote {
userId String
promptId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
@@id([userId, promptId])
@@index([userId])
@@index([promptId])
@@map("prompt_votes")
}
model PinnedPrompt {
userId String
promptId String
order Int @default(0)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
@@id([userId, promptId])
@@index([userId])
@@map("pinned_prompts")
}
// ============================================
// Prompt Reports
// ============================================
enum ReportReason { enum ReportReason {
SPAM SPAM
INAPPROPRIATE INAPPROPRIATE
@@ -316,47 +306,8 @@ enum ReportStatus {
DISMISSED DISMISSED
} }
model PromptReport {
id String @id @default(cuid())
reason ReportReason
details String? @db.Text
status ReportStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
promptId String
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
reporterId String
reporter User @relation(fields: [reporterId], references: [id], onDelete: Cascade)
@@index([promptId])
@@index([reporterId])
@@index([status])
@@map("prompt_reports")
}
// ============================================
// Webhook Integration
// ============================================
enum WebhookEvent { enum WebhookEvent {
PROMPT_CREATED PROMPT_CREATED
PROMPT_UPDATED PROMPT_UPDATED
PROMPT_DELETED PROMPT_DELETED
} }
model WebhookConfig {
id String @id @default(cuid())
name String // e.g. "Slack Notifications"
url String // Webhook URL
method String @default("POST") // HTTP method
headers Json? // Custom headers as JSON object
payload String @db.Text // JSON template with placeholders
events WebhookEvent[] // Which events trigger this webhook
isEnabled Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("webhook_configs")
}
+65
View File
@@ -0,0 +1,65 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
// Toggle unlist status (admin only)
export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const session = await auth();
if (!session?.user) {
return NextResponse.json(
{ error: "unauthorized", message: "You must be logged in" },
{ status: 401 }
);
}
// Only admins can unlist prompts
if (session.user.role !== "ADMIN") {
return NextResponse.json(
{ error: "forbidden", message: "Only admins can unlist prompts" },
{ status: 403 }
);
}
// Check if prompt exists
const existing = await db.prompt.findUnique({
where: { id },
select: { id: true, isUnlisted: true },
});
if (!existing) {
return NextResponse.json(
{ error: "not_found", message: "Prompt not found" },
{ status: 404 }
);
}
// Toggle unlist status
const newUnlistedStatus = !existing.isUnlisted;
await db.prompt.update({
where: { id },
data: {
isUnlisted: newUnlistedStatus,
unlistedAt: newUnlistedStatus ? new Date() : null,
},
});
return NextResponse.json({
success: true,
isUnlisted: newUnlistedStatus,
message: newUnlistedStatus ? "Prompt unlisted" : "Prompt relisted"
});
} catch (error) {
console.error("Unlist prompt error:", error);
return NextResponse.json(
{ error: "server_error", message: "Something went wrong" },
{ status: 500 }
);
}
}
+1
View File
@@ -138,6 +138,7 @@ export async function GET(request: Request) {
const where: Record<string, unknown> = { const where: Record<string, unknown> = {
isPrivate: false, isPrivate: false,
isUnlisted: false, // Exclude unlisted prompts from public API
deletedAt: null, // Exclude soft-deleted prompts deletedAt: null, // Exclude soft-deleted prompts
}; };
+1
View File
@@ -28,6 +28,7 @@ export async function GET() {
const prompts = await db.prompt.findMany({ const prompts = await db.prompt.findMany({
where: { where: {
isPrivate: false, isPrivate: false,
isUnlisted: false, // Exclude unlisted prompts from CSV export
deletedAt: null, deletedAt: null,
}, },
select: { select: {
+10
View File
@@ -19,6 +19,7 @@ import { DeleteVersionButton } from "@/components/prompts/delete-version-button"
import { VersionCompareModal } from "@/components/prompts/version-compare-modal"; import { VersionCompareModal } from "@/components/prompts/version-compare-modal";
import { VersionCompareButton } from "@/components/prompts/version-compare-button"; import { VersionCompareButton } from "@/components/prompts/version-compare-button";
import { FeaturePromptButton } from "@/components/prompts/feature-prompt-button"; import { FeaturePromptButton } from "@/components/prompts/feature-prompt-button";
import { UnlistPromptButton } from "@/components/prompts/unlist-prompt-button";
import { MediaPreview } from "@/components/prompts/media-preview"; import { MediaPreview } from "@/components/prompts/media-preview";
import { ReportPromptDialog } from "@/components/prompts/report-prompt-dialog"; import { ReportPromptDialog } from "@/components/prompts/report-prompt-dialog";
@@ -117,6 +118,11 @@ export default async function PromptPage({ params }: PromptPageProps) {
notFound(); notFound();
} }
// Check if user can view unlisted prompt (only owner and admins can see)
if (prompt.isUnlisted && prompt.authorId !== session?.user?.id && session?.user?.role !== "ADMIN") {
notFound();
}
const isOwner = session?.user?.id === prompt.authorId; const isOwner = session?.user?.id === prompt.authorId;
const isAdmin = session?.user?.role === "ADMIN"; const isAdmin = session?.user?.role === "ADMIN";
const canEdit = isOwner || isAdmin; const canEdit = isOwner || isAdmin;
@@ -546,6 +552,10 @@ export default async function PromptPage({ params }: PromptPageProps) {
promptId={prompt.id} promptId={prompt.id}
isFeatured={prompt.isFeatured} isFeatured={prompt.isFeatured}
/> />
<UnlistPromptButton
promptId={prompt.id}
isUnlisted={prompt.isUnlisted}
/>
<Button variant="outline" size="sm" asChild> <Button variant="outline" size="sm" asChild>
<Link href={`/prompts/${id}/edit`}> <Link href={`/prompts/${id}/edit`}>
<Edit className="h-4 w-4 mr-2" /> <Edit className="h-4 w-4 mr-2" />
+2
View File
@@ -58,6 +58,8 @@ export default async function PromptsPage({ searchParams }: PromptsPageProps) {
// Build where clause based on filters // Build where clause based on filters
const where: Record<string, unknown> = { const where: Record<string, unknown> = {
isPrivate: false, isPrivate: false,
isUnlisted: false, // Exclude unlisted prompts
deletedAt: null, // Exclude soft-deleted prompts
}; };
if (params.q) { if (params.q) {
@@ -0,0 +1,63 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { EyeOff, Eye } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface UnlistPromptButtonProps {
promptId: string;
isUnlisted: boolean;
className?: string;
}
export function UnlistPromptButton({
promptId,
isUnlisted: initialUnlisted,
className,
}: UnlistPromptButtonProps) {
const t = useTranslations("prompts");
const [isUnlisted, setIsUnlisted] = useState(initialUnlisted);
const [isLoading, setIsLoading] = useState(false);
const handleToggle = async () => {
setIsLoading(true);
try {
const response = await fetch(`/api/prompts/${promptId}/unlist`, {
method: "POST",
});
if (response.ok) {
const data = await response.json();
setIsUnlisted(data.isUnlisted);
}
} catch (error) {
console.error("Error toggling unlist status:", error);
} finally {
setIsLoading(false);
}
};
return (
<Button
variant={isUnlisted ? "destructive" : "outline"}
size="sm"
onClick={handleToggle}
disabled={isLoading}
className={cn("gap-1.5", className)}
>
{isUnlisted ? (
<>
<Eye className="h-4 w-4" />
{t("relist")}
</>
) : (
<>
<EyeOff className="h-4 w-4" />
{t("unlist")}
</>
)}
</Button>
);
}