diff --git a/package.json b/package.json index ea62786f..a23ed109 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "dev": "vite --port 5174 --host", + "dev": "vite --port 3000", "build": "vite build && tsc", "preview": "vite preview", "test": "vitest run", @@ -65,4 +65,4 @@ "vitest": "^4.0.18", "web-vitals": "^5.1.0" } -} \ No newline at end of file +} diff --git a/src/api/forms.ts b/src/api/forms.ts index 3d1d147a..03e0becd 100644 --- a/src/api/forms.ts +++ b/src/api/forms.ts @@ -52,7 +52,6 @@ export interface Form { updatedAt?: string // Last update timestamp fields?: Array // Associated form fields (optional) responseCount?: number // Number of submissions received - draftCount?: number // Number of draft saves } // Standard API response wrapper for consistent error handling diff --git a/src/api/responses.ts b/src/api/responses.ts index 50cc6112..71997823 100644 --- a/src/api/responses.ts +++ b/src/api/responses.ts @@ -47,8 +47,8 @@ async function handleResponse(response: Response): Promise { console.error('Response Text:', response.statusText) throw new Error( errorData.message || - errorData.error || - `Request failed: ${response.statusText}`, + errorData.error || + `Request failed: ${response.statusText}`, ) } const result: ApiResponse = await response.json() @@ -129,14 +129,4 @@ export const responsesApi = { }) return handleResponse>(response) }, - - // GET /responses/received - Get all responses RECEIVED for forms owned by the user - getAllReceived: async (): Promise> => { - const response = await fetch(`${API_URL}/responses/received`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - }) - return handleResponse>(response) - }, } diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts deleted file mode 100644 index 5d3d680a..00000000 --- a/src/lib/mock-data.ts +++ /dev/null @@ -1,163 +0,0 @@ -export const MOCK_RESPONSES = [ - { - id: 'res_1', - formId: 'form_1', - formName: 'Customer Feedback', - responder: 'John Doe', - email: 'john.doe@example.com', - createdAt: '2026-03-01T10:00:00Z', - status: 'Completed', - answers: { - 'Full Name': 'John Doe', - 'Email': 'john.doe@example.com', - 'Feedback': 'Great form! Very easy to use.', - 'Rating': 5, - }, - }, - { - id: 'res_2', - formId: 'form_1', - formName: 'Customer Feedback', - responder: 'Jane Smith', - email: 'jane.smith@example.com', - createdAt: '2026-03-02T14:30:00Z', - status: 'Completed', - answers: { - 'Full Name': 'Jane Smith', - 'Email': 'jane.smith@example.com', - 'Feedback': 'Could use more options in the dropdown.', - 'Rating': 4, - }, - }, - { - id: 'res_3', - formId: 'form_2', - formName: 'User Survey', - responder: 'Bob Wilson', - email: 'bob.wilson@example.com', - createdAt: '2026-03-05T09:15:00Z', - status: 'Flagged', - answers: { - 'Survey Question 1': 'Yes', - 'Suggestions': 'Maybe add a dark mode?', - }, - }, - { - id: 'res_4', - formId: 'form_1', - formName: 'Customer Feedback', - responder: 'Alice Brown', - email: 'alice.brown@example.com', - createdAt: '2026-03-08T16:45:00Z', - status: 'Completed', - answers: { - 'Full Name': 'Alice Brown', - 'Email': 'alice.brown@example.com', - 'Feedback': 'Very professional design.', - 'Rating': 5, - }, - }, - { - id: 'res_5', - formId: 'form_3', - formName: 'Product Registration', - responder: 'Charlie Davis', - email: 'charlie.davis@example.com', - createdAt: '2026-03-09T11:20:00Z', - status: 'Processing', - answers: { - 'Experience': 'Excellent', - 'Comments': 'I love the glassmorphism effect.', - }, - }, - { - id: 'res_6', - formId: 'form_1', - formName: 'Customer Feedback', - responder: 'David Miller', - email: 'david@example.com', - createdAt: '2026-03-10T08:00:00Z', - status: 'Completed', - answers: { - 'Full Name': 'David Miller', - 'Email': 'david@example.com', - 'Feedback': 'Fast and reliable.', - 'Rating': 5, - }, - }, - { - id: 'res_7', - formId: 'form_1', - formName: 'Customer Feedback', - responder: 'Eve Wilson', - email: 'eve@example.com', - createdAt: '2026-03-10T14:22:00Z', - status: 'Pending', - answers: { - 'Full Name': 'Eve Wilson', - 'Email': 'eve@example.com', - 'Feedback': 'Simple and clean.', - 'Rating': 4, - }, - }, -]; - -export const MOCK_ANALYTICS_STATS = { - totalForms: 12, - totalResponses: 156, - activeUsers: 45, - totalReports: 24, - scheduledReports: 3, - responsesPerPeriod: [ - { period: 'Mon', count: 12 }, - { period: 'Tue', count: 18 }, - { period: 'Wed', count: 20 }, - { period: 'Thu', count: 14 }, - { period: 'Fri', count: 25 }, - { period: 'Sat', count: 30 }, - { period: 'Sun', count: 37 }, - ], -}; - -export const MOCK_REPORTS = [ - { - id: 'rep_1', - name: 'Quarterly Feedback Summary', - formId: 'form_1', - formName: 'Customer Feedback', - type: 'Summary', - format: 'PDF', - generatedAt: '2026-03-01T08:00:00Z', - status: 'Completed', - }, - { - id: 'rep_2', - name: 'User Engagement Deep Dive', - formId: 'form_2', - formName: 'User Survey', - type: 'Detailed', - format: 'CSV', - generatedAt: '2026-03-05T14:30:00Z', - status: 'Completed', - }, - { - id: 'rep_3', - name: 'Weekly Performance Metrics', - formId: 'form_1', - formName: 'Customer Feedback', - type: 'Analytics', - format: 'JSON', - generatedAt: '2026-03-09T10:15:00Z', - status: 'Scheduled', - }, - { - id: 'rep_4', - name: 'Registration Trend Analysis', - formId: 'form_3', - formName: 'Product Registration', - type: 'Analytics', - format: 'PDF', - generatedAt: '2026-03-10T16:22:00Z', - status: 'Completed', - }, -]; diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index bb7793d3..4767d43c 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -22,7 +22,6 @@ import { Route as LayoutEditorIndexRouteImport } from './routes/_layout.editor.i import { Route as LayoutAnalyticsIndexRouteImport } from './routes/_layout.analytics.index' import { Route as LayoutEditorFormIdRouteImport } from './routes/_layout.editor.$formId' import { Route as LayoutAnalyticsResponsesRouteImport } from './routes/_layout.analytics.responses' -import { Route as LayoutAnalyticsReportsRouteImport } from './routes/_layout.analytics.reports' const SignupRoute = SignupRouteImport.update({ id: '/signup', @@ -89,11 +88,6 @@ const LayoutAnalyticsResponsesRoute = path: '/responses', getParentRoute: () => LayoutAnalyticsRoute, } as any) -const LayoutAnalyticsReportsRoute = LayoutAnalyticsReportsRouteImport.update({ - id: '/reports', - path: '/reports', - getParentRoute: () => LayoutAnalyticsRoute, -} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -104,7 +98,6 @@ export interface FileRoutesByFullPath { '/dashboard': typeof LayoutDashboardRoute '/my-responses': typeof LayoutMyResponsesRoute '/form/$formId': typeof FormFormIdRoute - '/analytics/reports': typeof LayoutAnalyticsReportsRoute '/analytics/responses': typeof LayoutAnalyticsResponsesRoute '/editor/$formId': typeof LayoutEditorFormIdRoute '/analytics/': typeof LayoutAnalyticsIndexRoute @@ -118,7 +111,6 @@ export interface FileRoutesByTo { '/dashboard': typeof LayoutDashboardRoute '/my-responses': typeof LayoutMyResponsesRoute '/form/$formId': typeof FormFormIdRoute - '/analytics/reports': typeof LayoutAnalyticsReportsRoute '/analytics/responses': typeof LayoutAnalyticsResponsesRoute '/editor/$formId': typeof LayoutEditorFormIdRoute '/analytics': typeof LayoutAnalyticsIndexRoute @@ -135,7 +127,6 @@ export interface FileRoutesById { '/_layout/dashboard': typeof LayoutDashboardRoute '/_layout/my-responses': typeof LayoutMyResponsesRoute '/form/$formId': typeof FormFormIdRoute - '/_layout/analytics/reports': typeof LayoutAnalyticsReportsRoute '/_layout/analytics/responses': typeof LayoutAnalyticsResponsesRoute '/_layout/editor/$formId': typeof LayoutEditorFormIdRoute '/_layout/analytics/': typeof LayoutAnalyticsIndexRoute @@ -152,7 +143,6 @@ export interface FileRouteTypes { | '/dashboard' | '/my-responses' | '/form/$formId' - | '/analytics/reports' | '/analytics/responses' | '/editor/$formId' | '/analytics/' @@ -166,7 +156,6 @@ export interface FileRouteTypes { | '/dashboard' | '/my-responses' | '/form/$formId' - | '/analytics/reports' | '/analytics/responses' | '/editor/$formId' | '/analytics' @@ -182,7 +171,6 @@ export interface FileRouteTypes { | '/_layout/dashboard' | '/_layout/my-responses' | '/form/$formId' - | '/_layout/analytics/reports' | '/_layout/analytics/responses' | '/_layout/editor/$formId' | '/_layout/analytics/' @@ -291,24 +279,15 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAnalyticsResponsesRouteImport parentRoute: typeof LayoutAnalyticsRoute } - '/_layout/analytics/reports': { - id: '/_layout/analytics/reports' - path: '/reports' - fullPath: '/analytics/reports' - preLoaderRoute: typeof LayoutAnalyticsReportsRouteImport - parentRoute: typeof LayoutAnalyticsRoute - } } } interface LayoutAnalyticsRouteChildren { - LayoutAnalyticsReportsRoute: typeof LayoutAnalyticsReportsRoute LayoutAnalyticsResponsesRoute: typeof LayoutAnalyticsResponsesRoute LayoutAnalyticsIndexRoute: typeof LayoutAnalyticsIndexRoute } const LayoutAnalyticsRouteChildren: LayoutAnalyticsRouteChildren = { - LayoutAnalyticsReportsRoute: LayoutAnalyticsReportsRoute, LayoutAnalyticsResponsesRoute: LayoutAnalyticsResponsesRoute, LayoutAnalyticsIndexRoute: LayoutAnalyticsIndexRoute, } diff --git a/src/routes/_layout.analytics.index.tsx b/src/routes/_layout.analytics.index.tsx index ec69a7bd..9291a3a8 100644 --- a/src/routes/_layout.analytics.index.tsx +++ b/src/routes/_layout.analytics.index.tsx @@ -1,20 +1,7 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { createFileRoute } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' -import { - AlertCircle, - Calendar, - ChevronRight, - ClipboardList, - Clock, - Download, - FileEdit, - FileText, - Loader2, - Search, -} from 'lucide-react' -import { useMemo, useState } from 'react' +import { AlertCircle, ClipboardList, FileText, Loader2, Users } from 'lucide-react' import { formsApi } from '@/api/forms' -import { responsesApi } from '@/api/responses' import { Card, CardContent, @@ -22,21 +9,12 @@ import { CardHeader, CardTitle, } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Badge } from '@/components/ui/badge' export const Route = createFileRoute('/_layout/analytics/')({ component: AnalyticsOverviewPage, }) function AnalyticsOverviewPage() { - const navigate = useNavigate() - const [dateRange, setDateRange] = useState('') - const [selectedForm, setSelectedForm] = useState('all') - const [responderSearch, setResponderSearch] = useState('') - // Fetch all forms const { data: forms, @@ -47,81 +25,8 @@ function AnalyticsOverviewPage() { queryFn: () => formsApi.getAll(), }) - // Fetch all received responses - const { - data: allResponses = [], - isLoading: isResponsesLoading, - error: responsesError, - } = useQuery({ - queryKey: ['received-responses'], - queryFn: () => responsesApi.getAllReceived(), - }) - - // Filtering logic for responses - const filteredResponses = useMemo(() => { - return allResponses.filter(res => { - const matchesForm = selectedForm === 'all' || res.formId === selectedForm - const matchesResponder = responderSearch === '' || - res.responder.toLowerCase().includes(responderSearch.toLowerCase()) || - res.email.toLowerCase().includes(responderSearch.toLowerCase()) - const matchesDate = dateRange === '' || res.createdAt.startsWith(dateRange) - - return matchesForm && matchesResponder && matchesDate - }) - }, [allResponses, selectedForm, responderSearch, dateRange]) - - // Chart data calculation based on actual response dates grouped by weekday - const chartData = useMemo(() => { - const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] - const counts: Record = { Mon: 0, Tue: 0, Wed: 0, Thu: 0, Fri: 0, Sat: 0, Sun: 0 } - - filteredResponses.forEach(r => { - const date = new Date(r.createdAt) - const dayName = days[date.getDay()] - counts[dayName]++ - }) - - const orderedDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] - return orderedDays.map(label => ({ - label, - value: counts[label] - })) - }, [filteredResponses]) - - const handleExport = (type: 'csv' | 'json') => { - const data = filteredResponses - let blob: Blob - let filename = `analytics_export_${new Date().toISOString().split('T')[0]}` - - if (type === 'json') { - blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) - filename += '.json' - } else { - const csvRows = [ - ['Responder', 'Email', 'Form', 'Date', 'Status'].join(','), - ...data.map(r => [ - `"${r.responder}"`, - `"${r.email}"`, - `"${r.formName}"`, - new Date(r.createdAt).toLocaleDateString(), - `"${r.status}"` - ].join(',')) - ] - blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }) - filename += '.csv' - } - - const url = URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = filename - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - } - // Loading state - if (isFormsLoading || isResponsesLoading) { + if (isFormsLoading) { return (
@@ -135,7 +40,7 @@ function AnalyticsOverviewPage() { } // Error state - if (formsError || responsesError || !forms) { + if (formsError || !forms) { return (
@@ -144,7 +49,7 @@ function AnalyticsOverviewPage() {

Error Loading Analytics

- {formsError || responsesError ? String(formsError || responsesError) : 'Failed to load form analytics'} + {formsError ? String(formsError) : 'Failed to load form analytics'}

@@ -153,230 +58,111 @@ function AnalyticsOverviewPage() { const totalForms = forms.length const publishedForms = forms.filter((f) => f.isPublished).length - const totalResponses = filteredResponses.filter(r => r.isSubmitted).length - const draftSaves = filteredResponses.filter(r => !r.isSubmitted).length return ( -
+
-
-
-

- Analytics Overview -

-

- Track your forms and responses at a glance -

-
-
- - -
+
+

+ Analytics Overview +

+

+ Track your forms and responses at a glance +

{/* Stats Cards */}
- + - + Total Forms - +
{totalForms}
-

- +{publishedForms} published +

+ {publishedForms} published

- + - - Total Responses + + Published Forms - + -
{totalResponses}
-

- From actual submission dates +

{publishedForms}
+

+ Active and collecting responses

- + - - Draft Saves + + Draft Forms - + -
{draftSaves}
-

- In-progress submissions +

{totalForms - publishedForms}
+

+ Not yet published

- {/* Analytics Section */} -
- {/* Chart Header & Controls */} - - -
-
- Responses per Period - Activity overview for the current week -
-
-
- - setDateRange(e.target.value)} - /> -
- -
- - setResponderSearch(e.target.value)} - /> -
-
+ {/* Forms List */} + + + Recent Forms + + Your most recently created forms + + + + {forms.length === 0 ? ( +
+ No forms created yet. Create your first form to get started!
- - - {/* Horizontal Weekly Summary Overlay */} -
-
-
- - Weekly Summary -
-
- {chartData.map(d => ( -
- {d.label} - {d.value} -
- ))} -
-
-
- -
- {chartData.map((day) => ( -
-
+ ) : ( +
+ {forms.slice(0, 5).map((form) => ( +
+
+

{form.title}

+

+ Created {new Date(form.createdAt).toLocaleDateString()} +

+
+
+ + {form.isPublished ? 'Published' : 'Draft'} + +
))}
- - - - {/* Recent Responses Table */} - - -
- Recent Responses Preview - Latest submissions across your forms -
- -
- -
- - - - - - - - - - - - {filteredResponses.length === 0 ? ( - - - - ) : ( - filteredResponses.slice(0, 5).map((res) => ( - - - - - - - - )) - )} - -
ResponderForm NameKey AnswerStatusSubmitted
- No responses match your current filters. -
-
{res.responder}
-
{res.email}
-
- - {res.formName} - - -
- {Object.values(res.answers).find(val => val && String(val).trim() !== '') as string || 'No data provided'} -
-
- - {res.status} - - -
- {new Date(res.createdAt).toLocaleDateString()} -
-
-
-
-
-
+ )} + +
) } - diff --git a/src/routes/_layout.analytics.reports.tsx b/src/routes/_layout.analytics.reports.tsx deleted file mode 100644 index 7d5c3274..00000000 --- a/src/routes/_layout.analytics.reports.tsx +++ /dev/null @@ -1,391 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { useQuery } from '@tanstack/react-query' -import { - AlertCircle, - ArrowRight, - BarChart3, - Calendar, - ChevronDown, - Download, - FileText, - Layout, - Loader2, - PieChart, - Search, - X -} from 'lucide-react' -import { useMemo, useState } from 'react' -import { formsApi } from '@/api/forms' -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { MOCK_ANALYTICS_STATS, MOCK_REPORTS } from '@/lib/mock-data' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Badge } from '@/components/ui/badge' -import { useToast } from '@/hooks/use-toast' - -export const Route = createFileRoute('/_layout/analytics/reports')({ - component: ReportsPage, -}) - -function ReportsPage() { - const { toast } = useToast() - const [searchTerm, setSearchTerm] = useState('') - const [filterDate, setFilterDate] = useState('') - const [selectedForm, setSelectedForm] = useState('all') - const [selectedType, setSelectedType] = useState('all') - const [isGenerating, setIsGenerating] = useState(false) - - // Fetch all forms - const { - data: forms, - isLoading: isFormsLoading, - error: formsError, - } = useQuery({ - queryKey: ['forms'], - queryFn: () => formsApi.getAll(), - }) - - // Filtering logic - const filteredReports = useMemo(() => { - return MOCK_REPORTS.filter(rep => { - const matchesSearch = searchTerm === '' || - rep.name.toLowerCase().includes(searchTerm.toLowerCase()) - - const matchesDate = filterDate === '' || rep.generatedAt.startsWith(filterDate) - const matchesForm = selectedForm === 'all' || rep.formId === selectedForm - const matchesType = selectedType === 'all' || rep.type === selectedType - - return matchesSearch && matchesDate && matchesForm && matchesType - }) - }, [searchTerm, filterDate, selectedForm, selectedType]) - - const handleGenerateReport = () => { - setIsGenerating(true) - setTimeout(() => { - setIsGenerating(false) - toast({ - title: "Report Generated", - description: "Your report is ready for download.", - }) - }, 2000) - } - - const handleDownload = (format: string, name: string) => { - toast({ - title: "Downloading...", - description: `Preparing ${name}.${format.toLowerCase()}`, - }) - // Simulate download - const blob = new Blob(["Mock report content"], { type: 'text/plain' }) - const url = URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = `${name.replace(/\s+/g, '_').toLowerCase()}.${format.toLowerCase()}` - link.click() - } - - if (isFormsLoading) { - return ( -
-
- -

Loading reports dashboard...

-
-
- ) - } - - if (formsError || !forms) { - return ( -
-
-
- -
-

Error Loading Dashboard

-

- {formsError ? String(formsError) : 'Failed to load report data'} -

-
-
- ) - } - - return ( -
-
-
-
-

Form Reports

-

- Generate and download analytics reports for your forms -

-
-
- - {/* Summary Cards */} -
- - - - Total Reports - - - - -
{MOCK_ANALYTICS_STATS.totalReports}
-

Generated since project start

-
-
- - - - - - Last Generated - - - - -
Today, 4:22 PM
-

Quarterly Summary Report

-
-
- - - - - Export Volume - - - - -
1.2k
-

Total downloads recorded

-
-
-
- -
- {/* Generate Report Config */} - - - Generate New Report - Configure and run a custom analysis - - -
- - -
- -
- -
- setFilterDate(e.target.value)} - /> - -
-
- -
- -
- - - -
-
- - -
-
- - {/* Reports History */} - - -
-
- Recent Reports - Download previously generated files -
-
-
- - setSearchTerm(e.target.value)} - /> -
- -
-
-
- -
- - - - - - - - - - - {filteredReports.length === 0 ? ( - - - - ) : ( - filteredReports.map((rep) => ( - - - - - - - )) - )} - -
Report NameTypeGeneratedActions
- No reports found matching your filters. -
-
- {rep.name} -
-
- From: {rep.formName} -
-
- - {rep.type} - - -
- {new Date(rep.generatedAt).toLocaleDateString()} -
-
-
- - -
-
-
- {filteredReports.length > 0 && ( -
- -
- )} -
-
-
- - {/* Export All Section */} -
-
-

Quick Export Data

-

Download all collected responses in your preferred format

-
-
- - - -
-
-
-
- ) -} - diff --git a/src/routes/_layout.analytics.responses.tsx b/src/routes/_layout.analytics.responses.tsx index a85de558..167cab12 100644 --- a/src/routes/_layout.analytics.responses.tsx +++ b/src/routes/_layout.analytics.responses.tsx @@ -1,16 +1,6 @@ import { createFileRoute } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' -import { - AlertCircle, - ChevronDown, - Download, - FileText, - Filter, - Loader2, - Search, - X, -} from 'lucide-react' -import { useMemo, useState } from 'react' +import { AlertCircle, FileText, Loader2 } from 'lucide-react' import type { FormResponseForOwner } from '@/api/responses' import type { Form } from '@/api/forms'; import { responsesApi } from '@/api/responses' @@ -22,39 +12,12 @@ import { CardHeader, CardTitle, } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Badge } from '@/components/ui/badge' export const Route = createFileRoute('/_layout/analytics/responses')({ component: ResponsesPage, }) -// Extended interface for frontend simulation -interface SimulatedResponse extends FormResponseForOwner { - responder?: string; - email?: string; - createdAt: string; - status?: string; - isSubmitted?: boolean; -} - function ResponsesPage() { - const [searchTerm, setSearchTerm] = useState('') - const [filterDate, setFilterDate] = useState('') - const [selectedForm, setSelectedForm] = useState('all') - const [selectedStatus, setSelectedStatus] = useState('all') - const [showFilters, setShowFilters] = useState(false) - const [expandedAnswers, setExpandedAnswers] = useState>({}) - - const toggleExpand = (responseId: string) => { - setExpandedAnswers(prev => ({ - ...prev, - [responseId]: !prev[responseId] - })) - } - // Fetch all forms const { data: forms, @@ -65,94 +28,43 @@ function ResponsesPage() { queryFn: () => formsApi.getAll(), }) - // Aggregate responses for all forms + // Aggregate responses for all forms - must be called before any returns const { - data: allResponses = [], + data: allResponses, isLoading: isAllResponsesLoading, - } = useQuery({ - queryKey: ['received-responses'], - queryFn: () => responsesApi.getAllReceived(), + } = useQuery< + Array<{ formId: string; formTitle?: string; responses: Array }> + >({ + queryKey: ['analytics', 'all-responses', forms?.map((f) => f.id) ?? []], + queryFn: async () => { + if (!forms || forms.length === 0) return [] + const grouped = await Promise.all( + forms.map(async (f) => { + try { + const res = await responsesApi.getForForm(f.id) + return { formId: f.id, formTitle: f.title, responses: res } + } catch (e) { + // If fetching responses for a particular form fails, return empty + console.error('Failed to fetch responses for form', f.id, e) + return { formId: f.id, formTitle: f.title, responses: [] } + } + }), + ) + return grouped + }, + enabled: !!forms && forms.length > 0, + retry: false, }) - // Group responses by form for the UI - const groupedResponses = useMemo(() => { - return (forms || []).map(f => { - const responsesForForm = allResponses.filter((r: any) => r.formId === f.id) - return { - formId: f.id, - formTitle: f.title, - responses: responsesForForm - } - }).filter(group => group.responses.length > 0 || searchTerm === '') - }, [allResponses, forms, searchTerm]) - - // Filtering logic - const filteredGroups = useMemo(() => { - return groupedResponses.map(group => ({ - ...group, - responses: group.responses.filter(r => { - const answersStr = JSON.stringify(r.answers).toLowerCase(); - const matchesSearch = searchTerm === '' || - answersStr.includes(searchTerm.toLowerCase()) || - (r.responder && r.responder.toLowerCase().includes(searchTerm.toLowerCase())) || - (r.email && r.email.toLowerCase().includes(searchTerm.toLowerCase())) - - const matchesDate = filterDate === '' || (r.createdAt && r.createdAt.startsWith(filterDate)) - const matchesForm = selectedForm === 'all' || r.formId === selectedForm - const matchesStatus = selectedStatus === 'all' || r.status === selectedStatus - - return matchesSearch && matchesDate && matchesForm && matchesStatus - }) - })).filter(group => group.responses.length > 0) - }, [groupedResponses, searchTerm, filterDate, selectedForm, selectedStatus]) - - const handleExport = (type: 'csv' | 'json', group: { formTitle?: string; responses: Array }) => { - const data = group.responses - let blob: Blob - let filename = `${group.formTitle || 'responses'}_${new Date().toISOString().split('T')[0]}` - - if (type === 'json') { - blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) - filename += '.json' - } else { - const headers = Array.from(new Set(data.flatMap((r: SimulatedResponse) => Object.keys(r.answers)))) - const csvRows = [ - ['Responder', 'Email', 'Date', 'Status', ...headers].join(','), - ...data.map((r: SimulatedResponse) => [ - `"${r.responder || 'Anonymous'}"`, - `"${r.email || 'N/A'}"`, - new Date(r.createdAt).toLocaleDateString(), - `"${r.status || 'Submitted'}"`, - ...headers.map(h => `"${String((r.answers as Record)[h] || '').replace(/"/g, '""')}"`) - ].join(',')) - ] - blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }) - filename += '.csv' - } - - const url = URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = filename - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - } - - const clearFilters = () => { - setSearchTerm('') - setFilterDate('') - setSelectedForm('all') - setSelectedStatus('all') - } - // Loading state if (isFormsLoading) { return (
-

Loading responses...

+

+ Loading responses... +

) @@ -175,246 +87,89 @@ function ResponsesPage() { ) } - return ( -
-
-
-
-

Form Responses

-

- Manage and analyze collected data -

-
-
+ // Empty state + if (forms.length === 0) { + return ( +
+
+
+
+

No Forms Yet

+

+ Create a form first to collect responses. +

+
+ ) + } - {/* Filters */} - - -
-
- - setSearchTerm(e.target.value)} - /> -
-
- setFilterDate(e.target.value)} - /> - -
-
- - {showFilters && ( -
-
- - -
-
- - -
-
- -
-
- )} -
-
+ return ( +
+
+
+

Form Responses

+

+ View all submitted responses for your forms +

+
+ {/* Loading / Error handling */} {isAllResponsesLoading && ( -
- -
Synchronizing responses...
+
+ +
Loading responses...
)} {forms.length === 0 && ( -
-
- -
-

No forms found

-

- Create your first form to start collecting responses and view analytics. -

+
+ No forms found. Create a form first to collect responses.
)} - {filteredGroups.length > 0 ? ( + {/* Show aggregated responses grouped by form */} + {allResponses && allResponses.length > 0 ? (
- {filteredGroups.map((group) => ( - - -
-
- {group.formTitle || 'Untitled Form'} - - - - {group.responses.length} responses - - - - Live - - -
-
- - -
-
+ {allResponses.map((group) => ( + + + {group.formTitle || 'Untitled Form'} + + {group.responses.length + ? `${group.responses.length} response${group.responses.length === 1 ? '' : 's'}` + : 'No responses yet'} + - -
- - - - - - - - - - - {group.responses.map((response) => ( - - - - - - - ))} - -
ResponderAnswer SummaryStatusSubmitted
-
- {response.responder && response.responder !== 'Anonymous' ? response.responder : `Guest User`} -
-
- {response.email && response.email !== 'N/A' ? response.email : 'Public Session'} -
-
-
- {Object.entries(response.answers) - .slice(0, expandedAnswers[response.id] ? undefined : 2) - .map(([k, v]) => ( -
- {k}: - - {Array.isArray(v) ? v.join(', ') : String(v)} - -
- ))} - - {Object.keys(response.answers).length > 2 && ( - - )} -
-
- - {response.status} - - -
- {response.createdAt && !isNaN(Date.parse(response.createdAt)) - ? new Date(response.createdAt).toLocaleDateString() - : response.submittedAt && !isNaN(Date.parse(response.submittedAt)) - ? new Date(response.submittedAt).toLocaleDateString() - : '3/10/2026'} -
-
- {response.createdAt && !isNaN(Date.parse(response.createdAt)) - ? new Date(response.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - : '10:15 PM'} + + {group.responses.length > 0 ? ( +
+ {group.responses.map((response, idx) => ( +
+

Response #{idx + 1}

+
+ {Object.entries(response.answers).map(([k, v]) => ( +
+
{k}:
+
+ {Array.isArray(v) ? v.join(', ') : v === null || v === undefined ? '-' : String(v)} +
-
-
+ ))} +
+
+ ))} +
+ ) : ( +
No responses received yet.
+ )} ))}
) : ( !isAllResponsesLoading && ( -
-
- -
-

No matches found for your filters

-

- Try adjusting your search terms or filters to find what you're looking for. -

- -
+
No responses received yet.
) )}
@@ -422,3 +177,5 @@ function ResponsesPage() { ) } +// Note: individual per-form response card component was removed in favor of the +// aggregated view above. Keep this file focused on the aggregated responses UI. diff --git a/src/routes/_layout.tsx b/src/routes/_layout.tsx index c24a68dc..f8a329f2 100644 --- a/src/routes/_layout.tsx +++ b/src/routes/_layout.tsx @@ -53,13 +53,7 @@ function LayoutComponent() { if (currentPath.startsWith('/editor')) { return { parent: null, current: 'Editor' } } - if (currentPath === '/analytics') { - return { parent: 'Analytics', current: 'Overview' } - } - if (currentPath === '/analytics/responses') { - return { parent: 'Analytics', current: 'Responses' } - } - if (currentPath === '/analytics/reports') { + if (currentPath.startsWith('/analytics')) { return { parent: 'Analytics', current: 'Reports' } } if (currentPath.startsWith('/settings')) { diff --git a/src/routes/form.$formId.tsx b/src/routes/form.$formId.tsx index ab82956a..f085ac54 100644 --- a/src/routes/form.$formId.tsx +++ b/src/routes/form.$formId.tsx @@ -101,44 +101,10 @@ function FormResponsePage() { // This creates a unified data structure that matches our renderer's expectations const formWithFields = form ? { ...form, fields: formFields || [] } : null - const [lastSaved, setLastSaved] = useState(null) - - // Simulation: Load from localStorage if it exists for this form - useEffect(() => { - const savedData = localStorage.getItem(`form_draft_${formId}`) - if (savedData && Object.keys(responses).length === 0) { - try { - const parsed = JSON.parse(savedData) - setResponses(parsed) - setLastSaved(new Date()) - toast({ - title: 'Draft recovered', - description: 'We found an unsaved draft on this device.', - }) - } catch (e) { - console.error('Failed to parse saved draft', e) - } - } - }, [formId]) - - // Simulation: Auto-save to localStorage on response change - useEffect(() => { - if (Object.keys(responses).length > 0) { - const timer = setTimeout(() => { - localStorage.setItem(`form_draft_${formId}`, JSON.stringify(responses)) - setLastSaved(new Date()) - }, 2000) - return () => clearTimeout(timer) - } - }, [responses, formId]) - // Submit mutation (final submission) // Handles both new submissions and converting drafts to final submissions const submitMutation = useMutation({ mutationFn: async (data: Record) => { - // Simulation: Clear localStorage on submission - localStorage.removeItem(`form_draft_${formId}`) - if (draftResponseId) { // Convert existing draft to submitted state // This keeps the same response ID for continuity @@ -358,27 +324,14 @@ function FormResponsePage() { return (
-
-
-

- {formWithFields.title || formWithFields.name || 'Untitled Form'} -

- {formWithFields.description && ( -

- {formWithFields.description} -

- )} -
- {lastSaved && ( -
-
- - Auto-saved -
- - {lastSaved.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })} - -
+
+

+ {formWithFields.title || formWithFields.name || 'Untitled Form'} +

+ {formWithFields.description && ( +

+ {formWithFields.description} +

)}
@@ -731,8 +684,8 @@ function FormFieldRenderer({ onChange(star)} />