diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..c5c36a87
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,265 @@
+# AGENTS.md
+
+> Guidelines for AI coding agents working on this project.
+
+## Project Overview
+
+**prompts.chat** is a social platform for AI prompts built with Next.js 16. It allows users to share, discover, and collect prompts from the community. The project is open source and can be self-hosted with customizable branding, themes, and authentication.
+
+### Tech Stack
+
+- **Framework:** Next.js 16.0.7 (App Router) with React 19.2
+- **Language:** TypeScript 5
+- **Database:** PostgreSQL with Prisma ORM 6.19
+- **Authentication:** NextAuth.js 5 (beta) with pluggable providers (credentials, GitHub, Google, Azure)
+- **Styling:** Tailwind CSS 4 with Radix UI primitives
+- **UI Components:** shadcn/ui pattern (components in `src/components/ui/`)
+- **Internationalization:** next-intl with 11 supported locales
+- **Icons:** Lucide React
+- **Forms:** React Hook Form with Zod validation
+
+## Project Structure
+
+```
+/
+├── prisma/ # Database schema and migrations
+│ ├── schema.prisma # Prisma schema definition
+│ ├── migrations/ # Database migrations
+│ └── seed.ts # Database seeding script
+├── public/ # Static assets (logos, favicon)
+├── messages/ # i18n translation files (en.json, es.json, etc.)
+├── src/
+│ ├── app/ # Next.js App Router pages
+│ │ ├── (auth)/ # Auth pages (login, register)
+│ │ ├── [username]/ # User profile pages
+│ │ ├── admin/ # Admin dashboard
+│ │ ├── api/ # API routes
+│ │ ├── categories/ # Category pages
+│ │ ├── prompts/ # Prompt CRUD pages
+│ │ ├── feed/ # User feed
+│ │ ├── discover/ # Discovery page
+│ │ ├── settings/ # User settings
+│ │ └── tags/ # Tag pages
+│ ├── components/ # React components
+│ │ ├── admin/ # Admin-specific components
+│ │ ├── auth/ # Authentication components
+│ │ ├── categories/ # Category components
+│ │ ├── layout/ # Layout components (header, etc.)
+│ │ ├── prompts/ # Prompt-related components
+│ │ ├── providers/ # React context providers
+│ │ ├── settings/ # Settings components
+│ │ └── ui/ # shadcn/ui base components
+│ ├── lib/ # Utility libraries
+│ │ ├── ai/ # AI/OpenAI integration
+│ │ ├── auth/ # NextAuth configuration
+│ │ ├── config/ # Config type definitions
+│ │ ├── i18n/ # Internationalization setup
+│ │ ├── plugins/ # Plugin system (auth, storage)
+│ │ ├── db.ts # Prisma client instance
+│ │ └── utils.ts # Utility functions (cn)
+│ └── i18n/ # i18n request handler
+├── prompts.config.ts # Main application configuration
+├── prompts.csv # Community prompts data source
+└── package.json # Dependencies and scripts
+```
+
+## Commands
+
+```bash
+# Development
+npm run dev # Start development server (localhost:3000)
+npm run build # Build for production (runs prisma generate first)
+npm run start # Start production server
+npm run lint # Run ESLint
+
+# Database
+npm run db:generate # Generate Prisma client
+npm run db:migrate # Run database migrations
+npm run db:push # Push schema changes to database
+npm run db:studio # Open Prisma Studio
+npm run db:seed # Seed database with initial data
+
+# Type checking
+npx tsc --noEmit # Check TypeScript types without emitting
+```
+
+## Code Style Guidelines
+
+### TypeScript
+
+- Use TypeScript strict mode
+- Prefer explicit types over `any`
+- Use `interface` for object shapes, `type` for unions/intersections
+- Functions: `camelCase` (e.g., `getUserData`, `handleSubmit`)
+- Components: `PascalCase` (e.g., `PromptCard`, `AuthContent`)
+- Constants: `UPPER_SNAKE_CASE` for true constants
+- Files: `kebab-case.tsx` for components, `camelCase.ts` for utilities
+
+### React/Next.js
+
+- Use React Server Components by default
+- Add `"use client"` directive only when client interactivity is needed
+- Prefer server actions over API routes for mutations
+- Use `next-intl` for all user-facing strings (never hardcode text)
+- Import translations with `useTranslations()` or `getTranslations()`
+
+### Component Pattern
+
+```tsx
+// Client component example
+"use client";
+
+import { useTranslations } from "next-intl";
+import { Button } from "@/components/ui/button";
+
+interface MyComponentProps {
+ title: string;
+ onAction: () => void;
+}
+
+export function MyComponent({ title, onAction }: MyComponentProps) {
+ const t = useTranslations("namespace");
+
+ return (
+
+
{title}
+
+
+ );
+}
+```
+
+### Styling
+
+- Use Tailwind CSS utility classes
+- Follow mobile-first responsive design (`sm:`, `md:`, `lg:` breakpoints)
+- Use `cn()` utility from `@/lib/utils` for conditional classes
+- Prefer Radix UI primitives via shadcn/ui components
+- Keep component styling scoped and composable
+
+### Database
+
+- Use Prisma Client from `@/lib/db`
+- Always include proper `select` or `include` for relations
+- Use transactions for multi-step operations
+- Add indexes for frequently queried fields
+
+## Configuration
+
+The main configuration file is `prompts.config.ts`:
+
+- **branding:** Logo, name, and description
+- **theme:** Colors, border radius, UI variant
+- **auth:** Authentication providers array (credentials, github, google, azure)
+- **i18n:** Supported locales and default locale
+- **features:** Feature flags (privatePrompts, changeRequests, categories, tags, aiSearch)
+- **homepage:** Homepage customization and sponsors
+
+## Plugin System
+
+Authentication and storage use a plugin architecture:
+
+### Auth Plugins (`src/lib/plugins/auth/`)
+- `credentials.ts` - Email/password authentication
+- `github.ts` - GitHub OAuth
+- `google.ts` - Google OAuth
+- `azure.ts` - Microsoft Entra ID
+
+### Storage Plugins (`src/lib/plugins/storage/`)
+- `url.ts` - URL-based media (default)
+- `s3.ts` - AWS S3 storage
+
+## Internationalization
+
+- Translation files are in `messages/{locale}.json`
+- Currently supported: en, tr, es, zh, ja, ar, pt, fr, de, ko, it
+- Add new locales to `prompts.config.ts` i18n.locales array
+- Create corresponding translation file in `messages/`
+- Add language to selector in `src/components/layout/header.tsx`
+
+## Key Files
+
+| File | Purpose |
+|------|---------|
+| `prompts.config.ts` | Main app configuration |
+| `prisma/schema.prisma` | Database schema |
+| `src/lib/auth/index.ts` | NextAuth configuration |
+| `src/lib/db.ts` | Prisma client singleton |
+| `src/app/layout.tsx` | Root layout with providers |
+| `src/components/ui/` | Base UI components (shadcn) |
+
+## Boundaries
+
+### Always Do
+- Run `npm run lint` before committing
+- Use existing UI components from `src/components/ui/`
+- Add translations for all user-facing text
+- Follow existing code patterns and file structure
+- Use TypeScript strict types
+
+### Ask First
+- Database schema changes (require migrations)
+- Adding new dependencies
+- Modifying authentication flow
+- Changes to `prompts.config.ts` structure
+
+### Never Do
+- Commit secrets or API keys (use `.env`)
+- Modify `node_modules/` or generated files
+- Delete existing translations
+- Remove or weaken TypeScript types
+- Hardcode user-facing strings (use i18n)
+
+## Environment Variables
+
+Required in `.env`:
+```
+DATABASE_URL= # PostgreSQL connection string
+AUTH_SECRET= # NextAuth secret key
+```
+
+Optional OAuth (if using those providers):
+```
+AUTH_GITHUB_ID=
+AUTH_GITHUB_SECRET=
+AUTH_GOOGLE_ID=
+AUTH_GOOGLE_SECRET=
+AUTH_AZURE_AD_CLIENT_ID=
+AUTH_AZURE_AD_CLIENT_SECRET=
+AUTH_AZURE_AD_ISSUER=
+```
+
+Optional features:
+```
+OPENAI_API_KEY= # For AI-powered semantic search
+```
+
+## Testing
+
+Currently no automated tests. When implementing:
+- Place tests adjacent to source files or in `__tests__/` directories
+- Use descriptive test names
+- Mock external services (database, OAuth)
+
+## Common Tasks
+
+### Adding a new page
+1. Create route in `src/app/{route}/page.tsx`
+2. Use server component for data fetching
+3. Add translations to `messages/*.json`
+
+### Adding a new component
+1. Create in appropriate `src/components/{category}/` folder
+2. Export from component file (no barrel exports needed)
+3. Follow existing component patterns
+
+### Adding a new API route
+1. Create in `src/app/api/{route}/route.ts`
+2. Export appropriate HTTP method handlers (GET, POST, etc.)
+3. Use Zod for request validation
+4. Return proper JSON responses with status codes
+
+### Modifying database schema
+1. Update `prisma/schema.prisma`
+2. Run `npm run db:migrate` to create migration
+3. Update related TypeScript types if needed
diff --git a/messages/ar.json b/messages/ar.json
index 7213df10..64bac8d8 100644
--- a/messages/ar.json
+++ b/messages/ar.json
@@ -259,7 +259,29 @@
"categories": "التصنيفات",
"tags": "الوسوم",
"webhooks": "Webhooks",
- "import": "استيراد أوامر المجتمع"
+ "prompts": "الأوامر",
+ "reports": "البلاغات"
+ },
+ "reports": {
+ "title": "إدارة البلاغات",
+ "description": "مراجعة وإدارة الأوامر المبلغ عنها",
+ "prompt": "الأمر",
+ "reason": "السبب",
+ "reportedBy": "المبلغ",
+ "status": "الحالة",
+ "date": "التاريخ",
+ "noReports": "لا توجد بلاغات بعد",
+ "viewPrompt": "عرض الأمر",
+ "markReviewed": "تعيين كمراجع",
+ "dismiss": "رفض",
+ "markedReviewed": "تم تعيين البلاغ كمراجع",
+ "dismissed": "تم رفض البلاغ",
+ "updateFailed": "فشل في تحديث البلاغ",
+ "statuses": {
+ "pending": "قيد الانتظار",
+ "reviewed": "تمت المراجعة",
+ "dismissed": "مرفوض"
+ }
},
"users": {
"title": "إدارة المستخدمين",
@@ -502,5 +524,27 @@
"browsePrompts": "تصفح الأوامر",
"categories": "التصنيفات",
"createPrompt": "إنشاء أمر"
+ },
+ "report": {
+ "report": "إبلاغ",
+ "reportPrompt": "الإبلاغ عن الأمر",
+ "reportDescription": "ساعدنا في الحفاظ على أمان المجتمع من خلال الإبلاغ عن المحتوى غير المناسب.",
+ "reason": "السبب",
+ "selectReason": "اختر سبباً",
+ "reasons": {
+ "spam": "رسائل مزعجة أو إعلانات",
+ "inappropriate": "محتوى غير لائق",
+ "copyright": "انتهاك حقوق النشر",
+ "misleading": "معلومات مضللة أو كاذبة",
+ "other": "أخرى"
+ },
+ "details": "تفاصيل إضافية",
+ "detailsPlaceholder": "يرجى تقديم مزيد من المعلومات حول هذا البلاغ...",
+ "optional": "اختياري",
+ "submitReport": "إرسال البلاغ",
+ "reportSubmitted": "تم إرسال البلاغ بنجاح",
+ "reportFailed": "فشل في إرسال البلاغ",
+ "reasonRequired": "يرجى اختيار سبب",
+ "alreadyReported": "لقد أبلغت عن هذا الأمر بالفعل"
}
}
diff --git a/messages/de.json b/messages/de.json
index 1138141b..d05916b5 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -273,7 +273,29 @@
"categories": "Kategorien",
"tags": "Tags",
"webhooks": "Webhooks",
- "prompts": "Prompts"
+ "prompts": "Prompts",
+ "reports": "Meldungen"
+ },
+ "reports": {
+ "title": "Meldungsverwaltung",
+ "description": "Gemeldete Prompts überprüfen und verwalten",
+ "prompt": "Prompt",
+ "reason": "Grund",
+ "reportedBy": "Gemeldet von",
+ "status": "Status",
+ "date": "Datum",
+ "noReports": "Noch keine Meldungen",
+ "viewPrompt": "Prompt anzeigen",
+ "markReviewed": "Als überprüft markieren",
+ "dismiss": "Ablehnen",
+ "markedReviewed": "Meldung als überprüft markiert",
+ "dismissed": "Meldung abgelehnt",
+ "updateFailed": "Fehler beim Aktualisieren der Meldung",
+ "statuses": {
+ "pending": "Ausstehend",
+ "reviewed": "Überprüft",
+ "dismissed": "Abgelehnt"
+ }
},
"prompts": {
"title": "Prompt-Verwaltung",
@@ -601,5 +623,27 @@
"browsePrompts": "Prompts durchsuchen",
"categories": "Kategorien",
"createPrompt": "Prompt erstellen"
+ },
+ "report": {
+ "report": "Melden",
+ "reportPrompt": "Prompt melden",
+ "reportDescription": "Helfen Sie uns, die Community sicher zu halten, indem Sie unangemessene Inhalte melden.",
+ "reason": "Grund",
+ "selectReason": "Wählen Sie einen Grund",
+ "reasons": {
+ "spam": "Spam oder Werbung",
+ "inappropriate": "Unangemessener Inhalt",
+ "copyright": "Urheberrechtsverletzung",
+ "misleading": "Irreführende oder falsche Informationen",
+ "other": "Sonstiges"
+ },
+ "details": "Zusätzliche Details",
+ "detailsPlaceholder": "Bitte geben Sie mehr Kontext zu dieser Meldung an...",
+ "optional": "optional",
+ "submitReport": "Meldung absenden",
+ "reportSubmitted": "Meldung erfolgreich gesendet",
+ "reportFailed": "Meldung konnte nicht gesendet werden",
+ "reasonRequired": "Bitte wählen Sie einen Grund",
+ "alreadyReported": "Sie haben diesen Prompt bereits gemeldet"
}
}
diff --git a/messages/en.json b/messages/en.json
index a7cd97e6..3634f954 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -273,7 +273,29 @@
"categories": "Categories",
"tags": "Tags",
"webhooks": "Webhooks",
- "prompts": "Prompts"
+ "prompts": "Prompts",
+ "reports": "Reports"
+ },
+ "reports": {
+ "title": "Reports Management",
+ "description": "Review and manage reported prompts",
+ "prompt": "Prompt",
+ "reason": "Reason",
+ "reportedBy": "Reported By",
+ "status": "Status",
+ "date": "Date",
+ "noReports": "No reports yet",
+ "viewPrompt": "View Prompt",
+ "markReviewed": "Mark as Reviewed",
+ "dismiss": "Dismiss",
+ "markedReviewed": "Report marked as reviewed",
+ "dismissed": "Report dismissed",
+ "updateFailed": "Failed to update report",
+ "statuses": {
+ "pending": "Pending",
+ "reviewed": "Reviewed",
+ "dismissed": "Dismissed"
+ }
},
"prompts": {
"title": "Prompts Management",
@@ -601,5 +623,27 @@
"browsePrompts": "Browse Prompts",
"categories": "Categories",
"createPrompt": "Create Prompt"
+ },
+ "report": {
+ "report": "Report",
+ "reportPrompt": "Report Prompt",
+ "reportDescription": "Help us keep the community safe by reporting inappropriate content.",
+ "reason": "Reason",
+ "selectReason": "Select a reason",
+ "reasons": {
+ "spam": "Spam or advertising",
+ "inappropriate": "Inappropriate content",
+ "copyright": "Copyright violation",
+ "misleading": "Misleading or false information",
+ "other": "Other"
+ },
+ "details": "Additional details",
+ "detailsPlaceholder": "Please provide more context about this report...",
+ "optional": "optional",
+ "submitReport": "Submit Report",
+ "reportSubmitted": "Report submitted successfully",
+ "reportFailed": "Failed to submit report",
+ "reasonRequired": "Please select a reason",
+ "alreadyReported": "You have already reported this prompt"
}
}
diff --git a/messages/es.json b/messages/es.json
index f2e5f773..25fb2960 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -253,7 +253,29 @@
"categories": "Categorías",
"tags": "Etiquetas",
"webhooks": "Webhooks",
- "import": "Importar Prompts de la Comunidad"
+ "prompts": "Prompts",
+ "reports": "Reportes"
+ },
+ "reports": {
+ "title": "Gestión de Reportes",
+ "description": "Revisa y gestiona los prompts reportados",
+ "prompt": "Prompt",
+ "reason": "Motivo",
+ "reportedBy": "Reportado por",
+ "status": "Estado",
+ "date": "Fecha",
+ "noReports": "No hay reportes aún",
+ "viewPrompt": "Ver Prompt",
+ "markReviewed": "Marcar como Revisado",
+ "dismiss": "Descartar",
+ "markedReviewed": "Reporte marcado como revisado",
+ "dismissed": "Reporte descartado",
+ "updateFailed": "Error al actualizar el reporte",
+ "statuses": {
+ "pending": "Pendiente",
+ "reviewed": "Revisado",
+ "dismissed": "Descartado"
+ }
},
"users": {
"title": "Gestión de Usuarios",
@@ -519,5 +541,27 @@
"browsePrompts": "Explorar Prompts",
"categories": "Categorías",
"createPrompt": "Crear Prompt"
+ },
+ "report": {
+ "report": "Reportar",
+ "reportPrompt": "Reportar Prompt",
+ "reportDescription": "Ayúdanos a mantener la comunidad segura reportando contenido inapropiado.",
+ "reason": "Motivo",
+ "selectReason": "Selecciona un motivo",
+ "reasons": {
+ "spam": "Spam o publicidad",
+ "inappropriate": "Contenido inapropiado",
+ "copyright": "Violación de derechos de autor",
+ "misleading": "Información engañosa o falsa",
+ "other": "Otro"
+ },
+ "details": "Detalles adicionales",
+ "detailsPlaceholder": "Proporciona más contexto sobre este reporte...",
+ "optional": "opcional",
+ "submitReport": "Enviar Reporte",
+ "reportSubmitted": "Reporte enviado exitosamente",
+ "reportFailed": "Error al enviar el reporte",
+ "reasonRequired": "Por favor selecciona un motivo",
+ "alreadyReported": "Ya has reportado este prompt"
}
}
diff --git a/messages/fr.json b/messages/fr.json
index 5d8cd0d1..bde29339 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -273,7 +273,29 @@
"categories": "Catégories",
"tags": "Tags",
"webhooks": "Webhooks",
- "prompts": "Prompts"
+ "prompts": "Prompts",
+ "reports": "Signalements"
+ },
+ "reports": {
+ "title": "Gestion des Signalements",
+ "description": "Examiner et gérer les prompts signalés",
+ "prompt": "Prompt",
+ "reason": "Raison",
+ "reportedBy": "Signalé par",
+ "status": "Statut",
+ "date": "Date",
+ "noReports": "Aucun signalement pour l'instant",
+ "viewPrompt": "Voir le Prompt",
+ "markReviewed": "Marquer comme Examiné",
+ "dismiss": "Rejeter",
+ "markedReviewed": "Signalement marqué comme examiné",
+ "dismissed": "Signalement rejeté",
+ "updateFailed": "Échec de la mise à jour du signalement",
+ "statuses": {
+ "pending": "En attente",
+ "reviewed": "Examiné",
+ "dismissed": "Rejeté"
+ }
},
"prompts": {
"title": "Gestion des Prompts",
@@ -601,5 +623,27 @@
"browsePrompts": "Parcourir les Prompts",
"categories": "Catégories",
"createPrompt": "Créer un Prompt"
+ },
+ "report": {
+ "report": "Signaler",
+ "reportPrompt": "Signaler le Prompt",
+ "reportDescription": "Aidez-nous à maintenir la communauté en sécurité en signalant les contenus inappropriés.",
+ "reason": "Raison",
+ "selectReason": "Sélectionnez une raison",
+ "reasons": {
+ "spam": "Spam ou publicité",
+ "inappropriate": "Contenu inapproprié",
+ "copyright": "Violation des droits d'auteur",
+ "misleading": "Information trompeuse ou fausse",
+ "other": "Autre"
+ },
+ "details": "Détails supplémentaires",
+ "detailsPlaceholder": "Veuillez fournir plus de contexte sur ce signalement...",
+ "optional": "optionnel",
+ "submitReport": "Envoyer le Signalement",
+ "reportSubmitted": "Signalement envoyé avec succès",
+ "reportFailed": "Échec de l'envoi du signalement",
+ "reasonRequired": "Veuillez sélectionner une raison",
+ "alreadyReported": "Vous avez déjà signalé ce prompt"
}
}
diff --git a/messages/it.json b/messages/it.json
index de878bac..573b7ae2 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -273,7 +273,29 @@
"categories": "Categorie",
"tags": "Tag",
"webhooks": "Webhook",
- "prompts": "Prompt"
+ "prompts": "Prompt",
+ "reports": "Segnalazioni"
+ },
+ "reports": {
+ "title": "Gestione Segnalazioni",
+ "description": "Revisiona e gestisci i prompt segnalati",
+ "prompt": "Prompt",
+ "reason": "Motivo",
+ "reportedBy": "Segnalato da",
+ "status": "Stato",
+ "date": "Data",
+ "noReports": "Nessuna segnalazione",
+ "viewPrompt": "Visualizza Prompt",
+ "markReviewed": "Segna come Revisionato",
+ "dismiss": "Archivia",
+ "markedReviewed": "Segnalazione revisionata",
+ "dismissed": "Segnalazione archiviata",
+ "updateFailed": "Impossibile aggiornare la segnalazione",
+ "statuses": {
+ "pending": "In attesa",
+ "reviewed": "Revisionato",
+ "dismissed": "Archiviato"
+ }
},
"prompts": {
"title": "Gestione Prompt",
@@ -601,5 +623,27 @@
"browsePrompts": "Sfoglia Prompt",
"categories": "Categorie",
"createPrompt": "Crea Prompt"
+ },
+ "report": {
+ "report": "Segnala",
+ "reportPrompt": "Segnala Prompt",
+ "reportDescription": "Aiutaci a mantenere la community sicura segnalando contenuti inappropriati.",
+ "reason": "Motivo",
+ "selectReason": "Seleziona un motivo",
+ "reasons": {
+ "spam": "Spam o pubblicità",
+ "inappropriate": "Contenuto inappropriato",
+ "copyright": "Violazione del copyright",
+ "misleading": "Informazioni fuorvianti o false",
+ "other": "Altro"
+ },
+ "details": "Dettagli aggiuntivi",
+ "detailsPlaceholder": "Fornisci maggiori dettagli su questa segnalazione...",
+ "optional": "opzionale",
+ "submitReport": "Invia Segnalazione",
+ "reportSubmitted": "Segnalazione inviata con successo",
+ "reportFailed": "Impossibile inviare la segnalazione",
+ "reasonRequired": "Seleziona un motivo",
+ "alreadyReported": "Hai già segnalato questo prompt"
}
}
diff --git a/messages/ja.json b/messages/ja.json
index df822103..575f3526 100644
--- a/messages/ja.json
+++ b/messages/ja.json
@@ -253,7 +253,29 @@
"categories": "カテゴリー",
"tags": "タグ",
"webhooks": "Webhook",
- "import": "コミュニティプロンプトをインポート"
+ "prompts": "プロンプト",
+ "reports": "報告"
+ },
+ "reports": {
+ "title": "報告管理",
+ "description": "報告されたプロンプトを確認・管理",
+ "prompt": "プロンプト",
+ "reason": "理由",
+ "reportedBy": "報告者",
+ "status": "ステータス",
+ "date": "日付",
+ "noReports": "報告はまだありません",
+ "viewPrompt": "プロンプトを見る",
+ "markReviewed": "確認済みにする",
+ "dismiss": "却下",
+ "markedReviewed": "報告を確認済みにしました",
+ "dismissed": "報告を却下しました",
+ "updateFailed": "報告の更新に失敗しました",
+ "statuses": {
+ "pending": "保留中",
+ "reviewed": "確認済み",
+ "dismissed": "却下済み"
+ }
},
"users": {
"title": "ユーザー管理",
@@ -496,5 +518,27 @@
"browsePrompts": "プロンプトを見る",
"categories": "カテゴリー",
"createPrompt": "プロンプトを作成"
+ },
+ "report": {
+ "report": "報告",
+ "reportPrompt": "プロンプトを報告",
+ "reportDescription": "不適切なコンテンツを報告して、コミュニティの安全を守るためにご協力ください。",
+ "reason": "理由",
+ "selectReason": "理由を選択",
+ "reasons": {
+ "spam": "スパムまたは広告",
+ "inappropriate": "不適切なコンテンツ",
+ "copyright": "著作権侵害",
+ "misleading": "誤解を招く情報または虚偽情報",
+ "other": "その他"
+ },
+ "details": "詳細情報",
+ "detailsPlaceholder": "この報告についての詳細を入力してください...",
+ "optional": "任意",
+ "submitReport": "報告を送信",
+ "reportSubmitted": "報告が送信されました",
+ "reportFailed": "報告の送信に失敗しました",
+ "reasonRequired": "理由を選択してください",
+ "alreadyReported": "このプロンプトは既に報告済みです"
}
}
diff --git a/messages/ko.json b/messages/ko.json
index 7a2060b5..11ae8c76 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -273,7 +273,29 @@
"categories": "카테고리",
"tags": "태그",
"webhooks": "웹훅",
- "prompts": "프롬프트"
+ "prompts": "프롬프트",
+ "reports": "신고"
+ },
+ "reports": {
+ "title": "신고 관리",
+ "description": "신고된 프롬프트를 검토하고 관리합니다",
+ "prompt": "프롬프트",
+ "reason": "사유",
+ "reportedBy": "신고자",
+ "status": "상태",
+ "date": "날짜",
+ "noReports": "아직 신고가 없습니다",
+ "viewPrompt": "프롬프트 보기",
+ "markReviewed": "검토 완료로 표시",
+ "dismiss": "기각",
+ "markedReviewed": "신고가 검토 완료로 표시되었습니다",
+ "dismissed": "신고가 기각되었습니다",
+ "updateFailed": "신고 업데이트에 실패했습니다",
+ "statuses": {
+ "pending": "대기 중",
+ "reviewed": "검토 완료",
+ "dismissed": "기각됨"
+ }
},
"prompts": {
"title": "프롬프트 관리",
@@ -601,5 +623,27 @@
"browsePrompts": "프롬프트 둘러보기",
"categories": "카테고리",
"createPrompt": "프롬프트 만들기"
+ },
+ "report": {
+ "report": "신고",
+ "reportPrompt": "프롬프트 신고",
+ "reportDescription": "부적절한 콘텐츠를 신고하여 커뮤니티를 안전하게 유지하는 데 도움을 주세요.",
+ "reason": "사유",
+ "selectReason": "사유를 선택하세요",
+ "reasons": {
+ "spam": "스팸 또는 광고",
+ "inappropriate": "부적절한 콘텐츠",
+ "copyright": "저작권 침해",
+ "misleading": "오해의 소지가 있거나 허위 정보",
+ "other": "기타"
+ },
+ "details": "추가 세부사항",
+ "detailsPlaceholder": "이 신고에 대한 자세한 내용을 입력하세요...",
+ "optional": "선택사항",
+ "submitReport": "신고 제출",
+ "reportSubmitted": "신고가 성공적으로 제출되었습니다",
+ "reportFailed": "신고 제출에 실패했습니다",
+ "reasonRequired": "사유를 선택하세요",
+ "alreadyReported": "이미 이 프롬프트를 신고하셨습니다"
}
}
diff --git a/messages/pt.json b/messages/pt.json
index df78eb94..6323c282 100644
--- a/messages/pt.json
+++ b/messages/pt.json
@@ -273,7 +273,29 @@
"categories": "Categorias",
"tags": "Tags",
"webhooks": "Webhooks",
- "prompts": "Prompts"
+ "prompts": "Prompts",
+ "reports": "Denúncias"
+ },
+ "reports": {
+ "title": "Gestão de Denúncias",
+ "description": "Revise e gerencie prompts denunciados",
+ "prompt": "Prompt",
+ "reason": "Motivo",
+ "reportedBy": "Denunciado por",
+ "status": "Estado",
+ "date": "Data",
+ "noReports": "Nenhuma denúncia ainda",
+ "viewPrompt": "Ver Prompt",
+ "markReviewed": "Marcar como Revisado",
+ "dismiss": "Descartar",
+ "markedReviewed": "Denúncia marcada como revisada",
+ "dismissed": "Denúncia descartada",
+ "updateFailed": "Falha ao atualizar denúncia",
+ "statuses": {
+ "pending": "Pendente",
+ "reviewed": "Revisado",
+ "dismissed": "Descartado"
+ }
},
"prompts": {
"title": "Gerenciamento de Prompts",
@@ -601,5 +623,27 @@
"browsePrompts": "Ver Prompts",
"categories": "Categorias",
"createPrompt": "Criar Prompt"
+ },
+ "report": {
+ "report": "Denunciar",
+ "reportPrompt": "Denunciar Prompt",
+ "reportDescription": "Ajude-nos a manter a comunidade segura denunciando conteúdo inadequado.",
+ "reason": "Motivo",
+ "selectReason": "Selecione um motivo",
+ "reasons": {
+ "spam": "Spam ou publicidade",
+ "inappropriate": "Conteúdo inadequado",
+ "copyright": "Violação de direitos autorais",
+ "misleading": "Informação enganosa ou falsa",
+ "other": "Outro"
+ },
+ "details": "Detalhes adicionais",
+ "detailsPlaceholder": "Forneça mais contexto sobre esta denúncia...",
+ "optional": "opcional",
+ "submitReport": "Enviar Denúncia",
+ "reportSubmitted": "Denúncia enviada com sucesso",
+ "reportFailed": "Falha ao enviar denúncia",
+ "reasonRequired": "Por favor, selecione um motivo",
+ "alreadyReported": "Você já denunciou este prompt"
}
}
diff --git a/messages/tr.json b/messages/tr.json
index 1356caef..918629d8 100644
--- a/messages/tr.json
+++ b/messages/tr.json
@@ -273,7 +273,29 @@
"categories": "Kategoriler",
"tags": "Etiketler",
"webhooks": "Webhooklar",
- "prompts": "Promptlar"
+ "prompts": "Promptlar",
+ "reports": "Şikayetler"
+ },
+ "reports": {
+ "title": "Şikayet Yönetimi",
+ "description": "Şikayet edilen promptları incele ve yönet",
+ "prompt": "Prompt",
+ "reason": "Sebep",
+ "reportedBy": "Şikayet Eden",
+ "status": "Durum",
+ "date": "Tarih",
+ "noReports": "Henüz şikayet yok",
+ "viewPrompt": "Promptu Görüntüle",
+ "markReviewed": "İncelendi Olarak İşaretle",
+ "dismiss": "Reddet",
+ "markedReviewed": "Şikayet incelendi olarak işaretlendi",
+ "dismissed": "Şikayet reddedildi",
+ "updateFailed": "Şikayet güncellenemedi",
+ "statuses": {
+ "pending": "Beklemede",
+ "reviewed": "İncelendi",
+ "dismissed": "Reddedildi"
+ }
},
"prompts": {
"title": "Prompt Yönetimi",
@@ -601,5 +623,27 @@
"browsePrompts": "Promptlara Göz At",
"categories": "Kategoriler",
"createPrompt": "Prompt Oluştur"
+ },
+ "report": {
+ "report": "Şikayet Et",
+ "reportPrompt": "Promptu Şikayet Et",
+ "reportDescription": "Uygunsuz içerikleri bildirerek topluluğumuzu güvende tutmamıza yardımcı olun.",
+ "reason": "Sebep",
+ "selectReason": "Bir sebep seçin",
+ "reasons": {
+ "spam": "Spam veya reklam",
+ "inappropriate": "Uygunsuz içerik",
+ "copyright": "Telif hakkı ihlali",
+ "misleading": "Yanıltıcı veya yanlış bilgi",
+ "other": "Diğer"
+ },
+ "details": "Ek detaylar",
+ "detailsPlaceholder": "Bu şikayet hakkında daha fazla bilgi verin...",
+ "optional": "isteğe bağlı",
+ "submitReport": "Şikayeti Gönder",
+ "reportSubmitted": "Şikayet başarıyla gönderildi",
+ "reportFailed": "Şikayet gönderilemedi",
+ "reasonRequired": "Lütfen bir sebep seçin",
+ "alreadyReported": "Bu promptu zaten şikayet ettiniz"
}
}
diff --git a/messages/zh.json b/messages/zh.json
index 3cf40593..b6907162 100644
--- a/messages/zh.json
+++ b/messages/zh.json
@@ -253,7 +253,29 @@
"categories": "分类",
"tags": "标签",
"webhooks": "Webhooks",
- "import": "导入社区提示词"
+ "prompts": "提示词",
+ "reports": "举报"
+ },
+ "reports": {
+ "title": "举报管理",
+ "description": "审核和管理被举报的提示词",
+ "prompt": "提示词",
+ "reason": "原因",
+ "reportedBy": "举报人",
+ "status": "状态",
+ "date": "日期",
+ "noReports": "暂无举报",
+ "viewPrompt": "查看提示词",
+ "markReviewed": "标记为已审核",
+ "dismiss": "驳回",
+ "markedReviewed": "举报已标记为已审核",
+ "dismissed": "举报已驳回",
+ "updateFailed": "更新举报失败",
+ "statuses": {
+ "pending": "待处理",
+ "reviewed": "已审核",
+ "dismissed": "已驳回"
+ }
},
"users": {
"title": "用户管理",
@@ -496,5 +518,27 @@
"browsePrompts": "浏览提示词",
"categories": "分类",
"createPrompt": "创建提示词"
+ },
+ "report": {
+ "report": "举报",
+ "reportPrompt": "举报提示词",
+ "reportDescription": "通过举报不当内容,帮助我们维护社区安全。",
+ "reason": "原因",
+ "selectReason": "选择原因",
+ "reasons": {
+ "spam": "垃圾信息或广告",
+ "inappropriate": "不当内容",
+ "copyright": "侵犯版权",
+ "misleading": "误导性或虚假信息",
+ "other": "其他"
+ },
+ "details": "详细说明",
+ "detailsPlaceholder": "请提供更多关于此举报的信息...",
+ "optional": "可选",
+ "submitReport": "提交举报",
+ "reportSubmitted": "举报提交成功",
+ "reportFailed": "举报提交失败",
+ "reasonRequired": "请选择举报原因",
+ "alreadyReported": "您已经举报过此提示词"
}
}
diff --git a/prisma/migrations/20251213100000_add_verified_and_reports/migration.sql b/prisma/migrations/20251213100000_add_verified_and_reports/migration.sql
new file mode 100644
index 00000000..6711589e
--- /dev/null
+++ b/prisma/migrations/20251213100000_add_verified_and_reports/migration.sql
@@ -0,0 +1,45 @@
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "verified" BOOLEAN NOT NULL DEFAULT false;
+
+-- CreateEnum (if not exists)
+DO $$ BEGIN
+ CREATE TYPE "ReportReason" AS ENUM ('SPAM', 'INAPPROPRIATE', 'COPYRIGHT', 'MISLEADING', 'OTHER');
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+
+-- CreateEnum (if not exists)
+DO $$ BEGIN
+ CREATE TYPE "ReportStatus" AS ENUM ('PENDING', 'REVIEWED', 'DISMISSED');
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+
+-- CreateTable (if not exists)
+CREATE TABLE IF NOT EXISTS "prompt_reports" (
+ "id" TEXT NOT NULL,
+ "reason" "ReportReason" NOT NULL,
+ "details" TEXT,
+ "status" "ReportStatus" NOT NULL DEFAULT 'PENDING',
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "promptId" TEXT NOT NULL,
+ "reporterId" TEXT NOT NULL,
+
+ CONSTRAINT "prompt_reports_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "prompt_reports_promptId_idx" ON "prompt_reports"("promptId");
+
+-- CreateIndex
+CREATE INDEX "prompt_reports_reporterId_idx" ON "prompt_reports"("reporterId");
+
+-- CreateIndex
+CREATE INDEX "prompt_reports_status_idx" ON "prompt_reports"("status");
+
+-- AddForeignKey
+ALTER TABLE "prompt_reports" ADD CONSTRAINT "prompt_reports_promptId_fkey" FOREIGN KEY ("promptId") REFERENCES "prompts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "prompt_reports" ADD CONSTRAINT "prompt_reports_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 11df2ecb..f741ce08 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -43,6 +43,7 @@ model User {
subscriptions CategorySubscription[]
votes PromptVote[]
pinnedPrompts PinnedPrompt[]
+ reports PromptReport[]
@@map("users")
}
@@ -153,6 +154,7 @@ model Prompt {
votes PromptVote[]
contributors User[] @relation("PromptContributors")
pinnedBy PinnedPrompt[]
+ reports PromptReport[]
@@index([authorId])
@@index([categoryId])
@@ -295,6 +297,44 @@ model PinnedPrompt {
@@map("pinned_prompts")
}
+// ============================================
+// Prompt Reports
+// ============================================
+
+enum ReportReason {
+ SPAM
+ INAPPROPRIATE
+ COPYRIGHT
+ MISLEADING
+ OTHER
+}
+
+enum ReportStatus {
+ PENDING
+ REVIEWED
+ 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
// ============================================
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index c2b8aa99..ab9ecab2 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -6,12 +6,13 @@ import { db } from "@/lib/db";
import { Prisma } from "@prisma/client";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Users, FolderTree, Tags, FileText, Webhook } from "lucide-react";
+import { Users, FolderTree, Tags, FileText, Webhook, Flag } from "lucide-react";
import { UsersTable } from "@/components/admin/users-table";
import { CategoriesTable } from "@/components/admin/categories-table";
import { TagsTable } from "@/components/admin/tags-table";
import { WebhooksTable } from "@/components/admin/webhooks-table";
import { PromptsManagement } from "@/components/admin/prompts-management";
+import { ReportsTable } from "@/components/admin/reports-table";
import { isAISearchEnabled } from "@/lib/ai/embeddings";
export const metadata: Metadata = {
@@ -49,7 +50,7 @@ export default async function AdminPage() {
}
// Fetch data for tables
- const [users, categories, tags, webhooks] = await Promise.all([
+ const [users, categories, tags, webhooks, reports] = await Promise.all([
db.user.findMany({
orderBy: { createdAt: "desc" },
select: {
@@ -98,6 +99,25 @@ export default async function AdminPage() {
db.webhookConfig.findMany({
orderBy: { createdAt: "desc" },
}),
+ db.promptReport.findMany({
+ orderBy: { createdAt: "desc" },
+ include: {
+ prompt: {
+ select: {
+ id: true,
+ title: true,
+ },
+ },
+ reporter: {
+ select: {
+ id: true,
+ username: true,
+ name: true,
+ avatar: true,
+ },
+ },
+ },
+ }),
]);
return (
@@ -170,6 +190,15 @@ export default async function AdminPage() {
{t("tabs.prompts")}
+
+
+ {t("tabs.reports")}
+ {reports.filter(r => r.status === "PENDING").length > 0 && (
+
+ {reports.filter(r => r.status === "PENDING").length}
+
+ )}
+
@@ -194,6 +223,10 @@ export default async function AdminPage() {
promptsWithoutEmbeddings={promptsWithoutEmbeddings}
/>
+
+
+
+
);
diff --git a/src/app/api/admin/reports/[id]/route.ts b/src/app/api/admin/reports/[id]/route.ts
new file mode 100644
index 00000000..48fd8a92
--- /dev/null
+++ b/src/app/api/admin/reports/[id]/route.ts
@@ -0,0 +1,37 @@
+import { NextResponse } from "next/server";
+import { z } from "zod";
+import { auth } from "@/lib/auth";
+import { db } from "@/lib/db";
+
+const updateSchema = z.object({
+ status: z.enum(["PENDING", "REVIEWED", "DISMISSED"]),
+});
+
+export async function PATCH(
+ request: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ const session = await auth();
+ if (!session?.user || session.user.role !== "ADMIN") {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { id } = await params;
+ const body = await request.json();
+ const { status } = updateSchema.parse(body);
+
+ const report = await db.promptReport.update({
+ where: { id },
+ data: { status },
+ });
+
+ return NextResponse.json(report);
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json({ error: "Invalid data" }, { status: 400 });
+ }
+ console.error("Report update error:", error);
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
+ }
+}
diff --git a/src/app/api/reports/route.ts b/src/app/api/reports/route.ts
new file mode 100644
index 00000000..83150865
--- /dev/null
+++ b/src/app/api/reports/route.ts
@@ -0,0 +1,80 @@
+import { NextResponse } from "next/server";
+import { z } from "zod";
+import { auth } from "@/lib/auth";
+import { db } from "@/lib/db";
+
+const reportSchema = z.object({
+ promptId: z.string().min(1),
+ reason: z.enum(["SPAM", "INAPPROPRIATE", "COPYRIGHT", "MISLEADING", "OTHER"]),
+ details: z.string().optional(),
+});
+
+export async function POST(request: Request) {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const body = await request.json();
+ const { promptId, reason, details } = reportSchema.parse(body);
+
+ // Check if prompt exists
+ const prompt = await db.prompt.findUnique({
+ where: { id: promptId },
+ select: { id: true, authorId: true },
+ });
+
+ if (!prompt) {
+ return NextResponse.json({ error: "Prompt not found" }, { status: 404 });
+ }
+
+ // Prevent self-reporting
+ if (prompt.authorId === session.user.id) {
+ return NextResponse.json(
+ { error: "You cannot report your own prompt" },
+ { status: 400 }
+ );
+ }
+
+ // Check if user already reported this prompt
+ const existingReport = await db.promptReport.findFirst({
+ where: {
+ promptId,
+ reporterId: session.user.id,
+ status: "PENDING",
+ },
+ });
+
+ if (existingReport) {
+ return NextResponse.json(
+ { error: "You have already reported this prompt" },
+ { status: 400 }
+ );
+ }
+
+ // Create the report
+ await db.promptReport.create({
+ data: {
+ promptId,
+ reporterId: session.user.id,
+ reason,
+ details: details || null,
+ },
+ });
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: "Invalid request data" },
+ { status: 400 }
+ );
+ }
+ console.error("Report creation error:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/prompts/[id]/page.tsx b/src/app/prompts/[id]/page.tsx
index 2d0b731c..e3878ec7 100644
--- a/src/app/prompts/[id]/page.tsx
+++ b/src/app/prompts/[id]/page.tsx
@@ -20,6 +20,7 @@ import { VersionCompareModal } from "@/components/prompts/version-compare-modal"
import { VersionCompareButton } from "@/components/prompts/version-compare-button";
import { FeaturePromptButton } from "@/components/prompts/feature-prompt-button";
import { MediaPreview } from "@/components/prompts/media-preview";
+import { ReportPromptDialog } from "@/components/prompts/report-prompt-dialog";
interface PromptPageProps {
params: Promise<{ id: string }>;
@@ -380,6 +381,12 @@ export default async function PromptPage({ params }: PromptPageProps) {
)}
+ {/* Report link */}
+ {!isOwner && (
+
+
+
+ )}
diff --git a/src/components/admin/reports-table.tsx b/src/components/admin/reports-table.tsx
new file mode 100644
index 00000000..554fb36a
--- /dev/null
+++ b/src/components/admin/reports-table.tsx
@@ -0,0 +1,211 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useTranslations, useLocale } from "next-intl";
+import Link from "next/link";
+import { formatDistanceToNow } from "@/lib/date";
+import { MoreHorizontal, Check, X, Eye, ExternalLink } from "lucide-react";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { toast } from "sonner";
+
+interface Report {
+ id: string;
+ reason: "SPAM" | "INAPPROPRIATE" | "COPYRIGHT" | "MISLEADING" | "OTHER";
+ details: string | null;
+ status: "PENDING" | "REVIEWED" | "DISMISSED";
+ createdAt: Date;
+ prompt: {
+ id: string;
+ title: string;
+ };
+ reporter: {
+ id: string;
+ username: string;
+ name: string | null;
+ avatar: string | null;
+ };
+}
+
+interface ReportsTableProps {
+ reports: Report[];
+}
+
+export function ReportsTable({ reports }: ReportsTableProps) {
+ const router = useRouter();
+ const t = useTranslations("admin.reports");
+ const tReport = useTranslations("report");
+ const locale = useLocale();
+ const [loading, setLoading] = useState(null);
+
+ const handleStatusChange = async (reportId: string, status: "REVIEWED" | "DISMISSED") => {
+ setLoading(reportId);
+ try {
+ const res = await fetch(`/api/admin/reports/${reportId}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ status }),
+ });
+
+ if (!res.ok) throw new Error("Failed to update status");
+
+ toast.success(status === "REVIEWED" ? t("markedReviewed") : t("dismissed"));
+ router.refresh();
+ } catch {
+ toast.error(t("updateFailed"));
+ } finally {
+ setLoading(null);
+ }
+ };
+
+ const statusColors = {
+ PENDING: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/20",
+ REVIEWED: "bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20",
+ DISMISSED: "bg-muted text-muted-foreground",
+ };
+
+ const reasonLabels: Record = {
+ SPAM: tReport("reasons.spam"),
+ INAPPROPRIATE: tReport("reasons.inappropriate"),
+ COPYRIGHT: tReport("reasons.copyright"),
+ MISLEADING: tReport("reasons.misleading"),
+ OTHER: tReport("reasons.other"),
+ };
+
+ return (
+ <>
+
+
+
{t("title")}
+
{t("description")}
+
+
+
+ {reports.length === 0 ? (
+
+ {t("noReports")}
+
+ ) : (
+
+
+
+
+ {t("prompt")}
+ {t("reason")}
+ {t("reportedBy")}
+ {t("status")}
+ {t("date")}
+
+
+
+
+ {reports.map((report) => (
+
+
+
+ {report.prompt.title}
+
+
+
+
+
+
{reasonLabels[report.reason]}
+ {report.details && (
+
+ {report.details}
+
+ )}
+
+
+
+
+
+
+
+ {report.reporter.name?.charAt(0) || report.reporter.username.charAt(0)}
+
+
+
@{report.reporter.username}
+
+
+
+
+ {t(`statuses.${report.status.toLowerCase()}`)}
+
+
+
+ {formatDistanceToNow(report.createdAt, locale)}
+
+
+
+
+
+
+
+
+
+
+ {t("viewPrompt")}
+
+
+
+ {report.status === "PENDING" && (
+ <>
+ handleStatusChange(report.id, "REVIEWED")}>
+
+ {t("markReviewed")}
+
+ handleStatusChange(report.id, "DISMISSED")}>
+
+ {t("dismiss")}
+
+ >
+ )}
+ {report.status !== "PENDING" && (
+ handleStatusChange(report.id, "REVIEWED")}
+ disabled={report.status === "REVIEWED"}
+ >
+
+ {t("markReviewed")}
+
+ )}
+
+
+
+
+ ))}
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/components/prompts/report-prompt-dialog.tsx b/src/components/prompts/report-prompt-dialog.tsx
new file mode 100644
index 00000000..303d8038
--- /dev/null
+++ b/src/components/prompts/report-prompt-dialog.tsx
@@ -0,0 +1,130 @@
+"use client";
+
+import { useState } from "react";
+import { useTranslations } from "next-intl";
+import { Flag, Loader2 } from "lucide-react";
+import { toast } from "sonner";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+interface ReportPromptDialogProps {
+ promptId: string;
+ isLoggedIn: boolean;
+}
+
+const REPORT_REASONS = ["SPAM", "INAPPROPRIATE", "COPYRIGHT", "MISLEADING", "OTHER"] as const;
+
+export function ReportPromptDialog({ promptId, isLoggedIn }: ReportPromptDialogProps) {
+ const t = useTranslations("report");
+ const tCommon = useTranslations("common");
+ const [open, setOpen] = useState(false);
+ const [reason, setReason] = useState("");
+ const [details, setDetails] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleSubmit = async () => {
+ if (!reason) {
+ toast.error(t("reasonRequired"));
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const response = await fetch("/api/reports", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ promptId, reason, details }),
+ });
+
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.error || "Failed to submit report");
+ }
+
+ toast.success(t("reportSubmitted"));
+ setOpen(false);
+ setReason("");
+ setDetails("");
+ } catch (error) {
+ toast.error(error instanceof Error ? error.message : t("reportFailed"));
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (!isLoggedIn) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/lib/i18n/config.ts b/src/lib/i18n/config.ts
index c8a5ad23..2adca747 100644
--- a/src/lib/i18n/config.ts
+++ b/src/lib/i18n/config.ts
@@ -2,7 +2,7 @@
export const LOCALE_COOKIE = "NEXT_LOCALE";
// Supported locales - keep in sync with prompts.config.ts
-export const supportedLocales = ["en", "tr", "es", "zh", "ja", "ar", "pt", "fr", "de", "ko"];
+export const supportedLocales = ["en", "tr", "es", "zh", "ja", "ar", "pt", "fr", "it", "de", "ko"];
export const defaultLocale = "en";
// RTL locales