mirror of
https://github.com/f/awesome-chatgpt-prompts.git
synced 2026-02-12 15:52:47 +00:00
docs(AGENTS.md): Add guidelines for AI coding agents working on the project
This commit is contained in:
265
AGENTS.md
Normal file
265
AGENTS.md
Normal file
@@ -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 (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
<Button onClick={onAction}>{t("actionLabel")}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
@@ -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": "لقد أبلغت عن هذا الأمر بالفعل"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "このプロンプトは既に報告済みです"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "이미 이 프롬프트를 신고하셨습니다"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "您已经举报过此提示词"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
// ============================================
|
||||
|
||||
@@ -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() {
|
||||
<FileText className="h-4 w-4" />
|
||||
{t("tabs.prompts")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="reports" className="gap-2">
|
||||
<Flag className="h-4 w-4" />
|
||||
{t("tabs.reports")}
|
||||
{reports.filter(r => r.status === "PENDING").length > 0 && (
|
||||
<span className="ml-1 px-1.5 py-0.5 text-xs bg-destructive text-white rounded-full">
|
||||
{reports.filter(r => r.status === "PENDING").length}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users">
|
||||
@@ -194,6 +223,10 @@ export default async function AdminPage() {
|
||||
promptsWithoutEmbeddings={promptsWithoutEmbeddings}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="reports">
|
||||
<ReportsTable reports={reports} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
37
src/app/api/admin/reports/[id]/route.ts
Normal file
37
src/app/api/admin/reports/[id]/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
80
src/app/api/reports/route.ts
Normal file
80
src/app/api/reports/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
<InteractivePromptContent content={prompt.content} title={t("promptContent")} />
|
||||
)}
|
||||
</div>
|
||||
{/* Report link */}
|
||||
{!isOwner && (
|
||||
<div className="flex justify-end pt-2">
|
||||
<ReportPromptDialog promptId={prompt.id} isLoggedIn={!!session?.user} />
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="versions" className="mt-0">
|
||||
|
||||
211
src/components/admin/reports-table.tsx
Normal file
211
src/components/admin/reports-table.tsx
Normal file
@@ -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<string | null>(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<string, string> = {
|
||||
SPAM: tReport("reasons.spam"),
|
||||
INAPPROPRIATE: tReport("reasons.inappropriate"),
|
||||
COPYRIGHT: tReport("reasons.copyright"),
|
||||
MISLEADING: tReport("reasons.misleading"),
|
||||
OTHER: tReport("reasons.other"),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{t("title")}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t("description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{reports.length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground border rounded-md">
|
||||
{t("noReports")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("prompt")}</TableHead>
|
||||
<TableHead>{t("reason")}</TableHead>
|
||||
<TableHead>{t("reportedBy")}</TableHead>
|
||||
<TableHead>{t("status")}</TableHead>
|
||||
<TableHead>{t("date")}</TableHead>
|
||||
<TableHead className="w-[50px]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{reports.map((report) => (
|
||||
<TableRow key={report.id}>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/prompts/${report.prompt.id}`}
|
||||
className="font-medium hover:underline flex items-center gap-1"
|
||||
>
|
||||
{report.prompt.title}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<Badge variant="outline">{reasonLabels[report.reason]}</Badge>
|
||||
{report.details && (
|
||||
<p className="text-xs text-muted-foreground mt-1 max-w-[200px] truncate">
|
||||
{report.details}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={report.reporter.avatar || undefined} />
|
||||
<AvatarFallback className="text-xs">
|
||||
{report.reporter.name?.charAt(0) || report.reporter.username.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm">@{report.reporter.username}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className={statusColors[report.status]}>
|
||||
{t(`statuses.${report.status.toLowerCase()}`)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{formatDistanceToNow(report.createdAt, locale)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
disabled={loading === report.id}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/prompts/${report.prompt.id}`}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
{t("viewPrompt")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{report.status === "PENDING" && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={() => handleStatusChange(report.id, "REVIEWED")}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
{t("markReviewed")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleStatusChange(report.id, "DISMISSED")}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
{t("dismiss")}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{report.status !== "PENDING" && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleStatusChange(report.id, "REVIEWED")}
|
||||
disabled={report.status === "REVIEWED"}
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
{t("markReviewed")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
130
src/components/prompts/report-prompt-dialog.tsx
Normal file
130
src/components/prompts/report-prompt-dialog.tsx
Normal file
@@ -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<string>("");
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<button className="text-xs text-muted-foreground hover:text-foreground hover:underline transition-colors inline-flex items-center gap-1">
|
||||
<Flag className="h-3 w-3" />
|
||||
{t("report")}
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("reportPrompt")}</DialogTitle>
|
||||
<DialogDescription>{t("reportDescription")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reason">{t("reason")}</Label>
|
||||
<Select value={reason} onValueChange={setReason}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t("selectReason")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{REPORT_REASONS.map((r) => (
|
||||
<SelectItem key={r} value={r}>
|
||||
{t(`reasons.${r.toLowerCase()}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="details">
|
||||
{t("details")} <span className="text-muted-foreground text-xs">({t("optional")})</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id="details"
|
||||
value={details}
|
||||
onChange={(e) => setDetails(e.target.value)}
|
||||
placeholder={t("detailsPlaceholder")}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)} disabled={isSubmitting}>
|
||||
{tCommon("cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting || !reason}>
|
||||
{isSubmitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
{t("submitReport")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user