feat(contact): add contact form with admin panel and navbar dropdown

Database changes:
- Added ContactMessage model to Prisma schema
- Fields: userId, username, firstName, lastName, email, subject, message, status, ipAddress
- Status enum: new, read, resolved
- Relation to User model

Backend changes:
- Added POST /api/public/contact endpoint for form submissions
- Works for both authenticated and non-authenticated users
- Validation for email, subject (3-255 chars), message (10-5000 chars)
- Activity logging for submissions
- Added admin endpoints:
  - GET /api/admin/contact-messages - list with filtering by status
  - GET /api/admin/contact-messages/:id - view single message (auto-marks as read)
  - PATCH /api/admin/contact-messages/:id/status - update status
  - DELETE /api/admin/contact-messages/:id - delete message

Frontend changes:
- Created ContactPage at /contact route
- For non-logged-in users: firstName, lastName, email, subject, message fields
- For logged-in users: auto-fills username, shows only email, subject, message
- Character counter for message (max 5000)
- Success screen with auto-redirect to homepage
- Created ContactMessagesPage at /admin/contact-messages
- Two-column layout: message list + detail view
- Filter by status (all, new, read, resolved)
- View message details with sender info and IP address
- Update status and delete messages
- Added admin dropdown menu to Navbar
  - Desktop: dropdown with Activity Logs and Contact Messages
  - Mobile: expandable submenu
  - Click outside to close on desktop
  - ChevronDown icon rotates when open

Note: CAPTCHA integration planned for future enhancement
This commit is contained in:
Radosław Gierwiało
2025-12-05 17:15:25 +01:00
parent f90945aa47
commit 34f18b3b50
8 changed files with 997 additions and 16 deletions

View File

@@ -77,6 +77,7 @@ model User {
heats EventUserHeat[]
recordingAssignments RecordingSuggestion[] @relation("RecorderAssignments")
activityLogs ActivityLog[]
contactMessages ContactMessage[]
@@map("users")
}
@@ -365,3 +366,37 @@ model ActivityLog {
@@map("activity_logs")
}
// Contact messages from users
model ContactMessage {
id Int @id @default(autoincrement())
// User identification (either logged in or anonymous)
userId Int? @map("user_id") // NULL for non-logged-in users
username String? @db.VarChar(50) // Username if logged in
firstName String? @map("first_name") @db.VarChar(100) // For non-logged-in users
lastName String? @map("last_name") @db.VarChar(100) // For non-logged-in users
email String @db.VarChar(255) // Always required
// Message content
subject String @db.VarChar(255)
message String @db.Text
// Admin tracking
status String @default("new") @db.VarChar(20) // 'new', 'read', 'resolved'
ipAddress String? @map("ip_address") @db.VarChar(45)
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
// Indexes
@@index([status, createdAt(sort: Desc)])
@@index([createdAt(sort: Desc)])
@@index([userId])
@@map("contact_messages")
}