diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index 813f4dcf..e6180756 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -20,6 +20,18 @@ import type { AdminUserCreateResponse, } from '@/types/admin' +export interface SurveyInviteResponse { + id: string + token: string + recipient_name: string + recipient_email: string | null + status: string + email_sent: boolean + created_at: string + completed_at: string | null + survey_url: string +} + export const adminApi = { // Dashboard getDashboardMetrics: () => @@ -140,6 +152,12 @@ export const adminApi = { api.put(`/admin/categories/global/${id}`, data).then(r => r.data), deleteGlobalCategory: (id: string) => api.delete(`/admin/categories/global/${id}`), + + // Survey Invites + listSurveyInvites: () => + api.get('/admin/survey-invites').then(r => r.data), + createSurveyInvite: (data: { recipient_name: string; recipient_email?: string; send_email?: boolean }) => + api.post('/admin/survey-invites', data).then(r => r.data), } export default adminApi diff --git a/frontend/src/components/admin/AdminSidebar.tsx b/frontend/src/components/admin/AdminSidebar.tsx index 1dc13723..19ff1ec8 100644 --- a/frontend/src/components/admin/AdminSidebar.tsx +++ b/frontend/src/components/admin/AdminSidebar.tsx @@ -8,6 +8,7 @@ import { ToggleLeft, Settings, FolderTree, + ClipboardList, ArrowLeft, } from 'lucide-react' import { cn } from '@/lib/utils' @@ -21,6 +22,7 @@ const navItems = [ { path: '/admin/feature-flags', label: 'Feature Flags', icon: ToggleLeft }, { path: '/admin/settings', label: 'Settings', icon: Settings }, { path: '/admin/categories', label: 'Categories', icon: FolderTree }, + { path: '/admin/survey-invites', label: 'Survey Invites', icon: ClipboardList }, ] interface AdminSidebarProps { diff --git a/frontend/src/pages/admin/SurveyInvitesPage.tsx b/frontend/src/pages/admin/SurveyInvitesPage.tsx new file mode 100644 index 00000000..595117e0 --- /dev/null +++ b/frontend/src/pages/admin/SurveyInvitesPage.tsx @@ -0,0 +1,206 @@ +import { useState, useEffect } from 'react' +import { adminApi } from '@/api/admin' +import type { SurveyInviteResponse } from '@/api/admin' +import { PageHeader } from '@/components/admin' +import { Copy, Check, Mail, Link2, Loader2, Send } from 'lucide-react' +import { cn } from '@/lib/utils' + +export default function SurveyInvitesPage() { + const [invites, setInvites] = useState([]) + const [loading, setLoading] = useState(true) + const [creating, setCreating] = useState(false) + const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [lastCreated, setLastCreated] = useState(null) + const [copied, setCopied] = useState(false) + const [error, setError] = useState('') + + const fetchInvites = async () => { + try { + const data = await adminApi.listSurveyInvites() + setInvites(data) + } catch { + setError('Failed to load invites') + } finally { + setLoading(false) + } + } + + useEffect(() => { fetchInvites() }, []) + + const handleCreate = async (sendEmail: boolean) => { + if (!name.trim()) return + if (sendEmail && !email.trim()) return + setCreating(true) + setError('') + try { + const invite = await adminApi.createSurveyInvite({ + recipient_name: name.trim(), + recipient_email: email.trim() || undefined, + send_email: sendEmail, + }) + setLastCreated(invite) + setName('') + setEmail('') + setInvites(prev => [invite, ...prev]) + } catch { + setError('Failed to create invite') + } finally { + setCreating(false) + } + } + + const handleCopy = async (url: string) => { + await navigator.clipboard.writeText(url) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString('en-US', { + month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', + }) + } + + return ( +
+ + + {/* Create Invite Section */} +
+

Create Invite

+
+
+ + setName(e.target.value)} + placeholder="John Smith" + className="w-full rounded-[10px] border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none" + /> +
+
+ + setEmail(e.target.value)} + placeholder="john@example.com" + className="w-full rounded-[10px] border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none" + /> +
+
+ + +
+
+ + {error && ( +

{error}

+ )} + + {lastCreated && ( +
+
+
+

+ {lastCreated.email_sent ? 'Email sent to ' + lastCreated.recipient_email + '! Link:' : 'Share this link with ' + lastCreated.recipient_name + ':'} +

+

{lastCreated.survey_url}

+
+ +
+
+ )} +
+ + {/* Invites Table */} +
+
+ + + + + + + + + + + + + + {loading ? ( + + ) : invites.length === 0 ? ( + + ) : ( + invites.map(invite => ( + + + + + + + + + + )) + )} + +
NameEmailStatusSentCreatedCompletedLink
Loading...
No invites yet
{invite.recipient_name}{invite.recipient_email || '—'} + + {invite.status} + + + {invite.email_sent ? ( + + ) : ( + + )} + {formatDate(invite.created_at)}{invite.completed_at ? formatDate(invite.completed_at) : '—'} + +
+
+
+
+ ) +} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 5afac932..0b530816 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -10,6 +10,7 @@ import { // Public pages const SharedSessionPage = lazy(() => import('@/pages/SharedSessionPage')) +const SurveyPage = lazy(() => import('@/pages/SurveyPage')) // Standalone auth pages const VerifyEmailPage = lazy(() => import('@/pages/VerifyEmailPage')) @@ -50,6 +51,7 @@ const AdminPlanLimitsPage = lazy(() => import('@/pages/admin/PlanLimitsPage')) const AdminFeatureFlagsPage = lazy(() => import('@/pages/admin/FeatureFlagsPage')) const AdminSettingsPage = lazy(() => import('@/pages/admin/SettingsPage')) const AdminGlobalCategoriesPage = lazy(() => import('@/pages/admin/GlobalCategoriesPage')) +const AdminSurveyInvitesPage = lazy(() => import('@/pages/admin/SurveyInvitesPage')) // Account pages const AccountLayout = lazy(() => import('@/components/account/AccountLayout')) @@ -96,6 +98,15 @@ export const router = createBrowserRouter([ ), errorElement: , }, + { + path: '/survey', + element: ( + }> + + + ), + errorElement: , + }, { path: '/share/:shareToken', element: ( @@ -384,6 +395,14 @@ export const router = createBrowserRouter([ ), }, + { + path: 'survey-invites', + element: ( + }> + + + ), + }, ], }, // Account routes