docs(AGENTS.md): Add guidelines for AI coding agents working on the project

This commit is contained in:
Fatih Kadir Akın
2025-12-13 13:04:24 +03:00
parent 381b000196
commit 407d410c3a
21 changed files with 1346 additions and 14 deletions

265
AGENTS.md Normal file
View 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

View File

@@ -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": "لقد أبلغت عن هذا الأمر بالفعل"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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": "このプロンプトは既に報告済みです"
}
}

View File

@@ -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": "이미 이 프롬프트를 신고하셨습니다"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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": "您已经举报过此提示词"
}
}

View File

@@ -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;

View File

@@ -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
// ============================================

View File

@@ -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>
);

View 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 });
}
}

View 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 }
);
}
}

View File

@@ -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">

View 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>
)}
</>
);
}

View 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>
);
}

View File

@@ -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