chore(api): sync prompts.csv from API and update message translations
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -103,6 +103,8 @@
|
|||||||
"promptPublic": "عام",
|
"promptPublic": "عام",
|
||||||
"feature": "تمييز",
|
"feature": "تمييز",
|
||||||
"featured": "مميز",
|
"featured": "مميز",
|
||||||
|
"unlist": "إلغاء الإدراج",
|
||||||
|
"relist": "إعادة الإدراج",
|
||||||
"adminArea": "منطقة المسؤول",
|
"adminArea": "منطقة المسؤول",
|
||||||
"types": {
|
"types": {
|
||||||
"text": "نص",
|
"text": "نص",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -101,6 +101,8 @@
|
|||||||
"promptPublic": "公開",
|
"promptPublic": "公開",
|
||||||
"feature": "注目に設定",
|
"feature": "注目に設定",
|
||||||
"featured": "注目",
|
"featured": "注目",
|
||||||
|
"unlist": "非公開にする",
|
||||||
|
"relist": "公開に戻す",
|
||||||
"adminArea": "管理者エリア",
|
"adminArea": "管理者エリア",
|
||||||
"noMorePrompts": "最後まで表示しました",
|
"noMorePrompts": "最後まで表示しました",
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
|
|||||||
@@ -103,6 +103,8 @@
|
|||||||
"promptPublic": "공개",
|
"promptPublic": "공개",
|
||||||
"feature": "추천",
|
"feature": "추천",
|
||||||
"featured": "추천됨",
|
"featured": "추천됨",
|
||||||
|
"unlist": "숨기기",
|
||||||
|
"relist": "다시 표시",
|
||||||
"adminArea": "관리자 영역",
|
"adminArea": "관리자 영역",
|
||||||
"types": {
|
"types": {
|
||||||
"text": "텍스트",
|
"text": "텍스트",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -101,6 +101,8 @@
|
|||||||
"promptPublic": "公开",
|
"promptPublic": "公开",
|
||||||
"feature": "精选",
|
"feature": "精选",
|
||||||
"featured": "已精选",
|
"featured": "已精选",
|
||||||
|
"unlist": "取消列出",
|
||||||
|
"relist": "重新列出",
|
||||||
"adminArea": "管理员区域",
|
"adminArea": "管理员区域",
|
||||||
"noMorePrompts": "已到达末尾",
|
"noMorePrompts": "已到达末尾",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
|
|||||||
+1
-1
@@ -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
@@ -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")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user