From 9ca8c68c3c82a5612514cbb49efe9f945668e889 Mon Sep 17 00:00:00 2001 From: Quinn Ftw Date: Sun, 28 Dec 2025 17:47:57 -0800 Subject: [PATCH] feat(platform-admin): add merch submission management pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add admin pages for managing merch submissions including review and approval workflows. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- features/platform-admin/frontend/src/App.tsx | 8 + .../merch/MerchSubmissionDetailModal.tsx | 231 +++++++++++++++++ .../src/pages/merch/MerchSubmissionsPage.tsx | 243 ++++++++++++++++++ .../frontend/src/pages/merch/index.ts | 2 + 4 files changed, 484 insertions(+) create mode 100644 features/platform-admin/frontend/src/pages/merch/MerchSubmissionDetailModal.tsx create mode 100644 features/platform-admin/frontend/src/pages/merch/MerchSubmissionsPage.tsx create mode 100644 features/platform-admin/frontend/src/pages/merch/index.ts diff --git a/features/platform-admin/frontend/src/App.tsx b/features/platform-admin/frontend/src/App.tsx index 4fa1bd581..324fdbf44 100644 --- a/features/platform-admin/frontend/src/App.tsx +++ b/features/platform-admin/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { Routes, Route, NavLink } from 'react-router-dom'; import { ScammersPage } from './pages/conversations/ScammersPage'; import { TrainingPage } from './pages/conversations/TrainingPage'; import { DevicesPage } from './pages/devices/DevicesPage'; +import { MerchSubmissionsPage } from './pages/merch/MerchSubmissionsPage'; import clsx from 'clsx'; const navSections = [ @@ -18,6 +19,12 @@ const navSections = [ { to: '/devices', label: 'Device Authorization' }, ], }, + { + title: 'Merch', + items: [ + { to: '/merch/submissions', label: 'Idea Submissions' }, + ], + }, ]; export function App() { @@ -71,6 +78,7 @@ export function App() { } /> } /> } /> + } /> diff --git a/features/platform-admin/frontend/src/pages/merch/MerchSubmissionDetailModal.tsx b/features/platform-admin/frontend/src/pages/merch/MerchSubmissionDetailModal.tsx new file mode 100644 index 000000000..582eccbd7 --- /dev/null +++ b/features/platform-admin/frontend/src/pages/merch/MerchSubmissionDetailModal.tsx @@ -0,0 +1,231 @@ +import { useState, useEffect } from 'react' +import { formatDistanceToNow } from 'date-fns' +import type { MerchSubmissionResponseDto } from '@lilith/types/api' + +interface MerchSubmissionDetailModalProps { + submission: MerchSubmissionResponseDto + onClose: () => void + onApprove: (notes?: string) => void + onReject: (notes?: string) => void + isUpdating: boolean +} + +const statusColors: Record = { + draft: 'bg-gray-500/20 text-gray-400', + pending: 'bg-yellow-500/20 text-yellow-400', + under_review: 'bg-blue-500/20 text-blue-400', + approved: 'bg-green-500/20 text-green-400', + rejected: 'bg-red-500/20 text-red-400', + implemented: 'bg-purple-500/20 text-purple-400', +} + +const statusLabels: Record = { + draft: 'Draft', + pending: 'Pending', + under_review: 'Under Review', + approved: 'Approved', + rejected: 'Rejected', + implemented: 'Implemented', +} + +export function MerchSubmissionDetailModal({ + submission, + onClose, + onApprove, + onReject, + isUpdating, +}: MerchSubmissionDetailModalProps) { + const [adminNotes, setAdminNotes] = useState(submission.adminNotes || '') + const [selectedImage, setSelectedImage] = useState(null) + + // Handle escape key + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + if (selectedImage) { + setSelectedImage(null) + } else { + onClose() + } + } + } + window.addEventListener('keydown', handleEscape) + return () => window.removeEventListener('keydown', handleEscape) + }, [onClose, selectedImage]) + + // Prevent body scroll + useEffect(() => { + document.body.style.overflow = 'hidden' + return () => { + document.body.style.overflow = '' + } + }, []) + + const canReview = submission.status === 'pending' || submission.status === 'under_review' + + return ( + <> + {/* Modal backdrop */} +
+ + {/* Modal content */} +
+ {/* Header */} +
+
+

Submission Details

+

ID: {submission.id}

+
+
+ + {statusLabels[submission.status]} + + +
+
+ + {/* Body */} +
+
+ {/* Left column - Details */} +
+ {/* Submitter info */} +
+

Submitter

+
{submission.submitterName || 'Anonymous'}
+ {submission.submitterEmail && ( +
{submission.submitterEmail}
+ )} +
+ Submitted {formatDistanceToNow(new Date(submission.submittedAt), { addSuffix: true })} +
+
+ + {/* Description */} +
+

Idea Description

+

{submission.description}

+
+ + {/* Admin notes */} +
+

Admin Notes

+