From 7db9626c1764ddfbf6998e3dbf798ee8f1eb6068 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 30 Dec 2024 00:47:35 -0500 Subject: [PATCH] Condense and restyle frontend component --- .../dashboard/TypeformDashboard.jsx | 641 +++++------------- 1 file changed, 178 insertions(+), 463 deletions(-) diff --git a/dashboard/src/components/dashboard/TypeformDashboard.jsx b/dashboard/src/components/dashboard/TypeformDashboard.jsx index c2a07a4..9cec777 100644 --- a/dashboard/src/components/dashboard/TypeformDashboard.jsx +++ b/dashboard/src/components/dashboard/TypeformDashboard.jsx @@ -7,7 +7,6 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Table, TableBody, @@ -20,17 +19,8 @@ import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Skeleton } from "@/components/ui/skeleton"; -import { AlertCircle, Activity, Eye, MessageSquare, ThumbsUp, Clock, Users, Star, ArrowUp, ArrowDown, BarChart3 } from "lucide-react"; +import { AlertCircle } from "lucide-react"; import { format } from "date-fns"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; import { BarChart, Bar, @@ -40,9 +30,6 @@ import { Tooltip, ResponsiveContainer, Cell, - PieChart, - Pie, - Legend, ReferenceLine } from "recharts"; @@ -57,12 +44,6 @@ const FORM_NAMES = { [FORM_IDS.FORM_2]: 'Winback Survey', }; -const METRIC_COLORS = { - total: "#8b5cf6", - completed: "#10b981", - today: "#f59e0b", -}; - // Loading skeleton components const SkeletonChart = () => (
@@ -102,9 +83,6 @@ const SkeletonTable = () => ( - - - @@ -117,10 +95,7 @@ const SkeletonTable = () => ( - - - - + ))} @@ -129,241 +104,6 @@ const SkeletonTable = () => (
); -const SkeletonMetricCard = () => ( - - -
-
- -
- - -
-
- -
-
-
-); - -const CustomTooltip = ({ active, payload }) => { - if (active && payload && payload.length) { - return ( - - - {payload.map((entry, index) => ( -
- {entry.name}: - {entry.value.toLocaleString()} -
- ))} -
-
- ); - } - return null; -}; - -const ResponseDialog = ({ response }) => ( - - - - - - - Response Details - - From {response.hidden?.name || 'Anonymous'} ({response.hidden?.email || 'No email'}) -
- Submitted at {format(new Date(response.submitted_at), "PPpp")} -
-
- -
- {response.answers?.map((answer, index) => ( -
-

{answer.field.type.replace(/_/g, ' ').toUpperCase()}

- {answer.type === 'boolean' && ( -

{answer.boolean ? 'Yes' : 'No'}

- )} - {answer.type === 'number' && ( -

Rating: {answer.number}/5

- )} - {answer.type === 'choices' && answer.choices?.labels && ( -

{answer.choices.labels.join(', ')}

- )} - {answer.type === 'text' && ( -

{answer.text}

- )} -
- ))} -
-
-
-
-); - -const getResponseSummary = (response) => { - if (!response.answers?.length) return 'No answers'; - - const firstAnswer = response.answers[0]; - switch (firstAnswer.type) { - case 'boolean': - return firstAnswer.boolean ? 'Yes' : 'No'; - case 'number': - return `Rating: ${firstAnswer.number}/5`; - case 'choices': - return firstAnswer.choices?.labels?.[0] || 'Multiple choice answer'; - case 'text': - return firstAnswer.text ? (firstAnswer.text.slice(0, 50) + (firstAnswer.text.length > 50 ? '...' : '')) : 'Text answer'; - default: - return `${firstAnswer.type} answer`; - } -}; - -const renderResponsesTable = (responses) => { - if (!responses?.items?.length) { - return ( -
-
- -
No responses yet
-
- Responses will appear here when they come in -
-
-
- ); - } - - return ( - - - Form Responses - - Latest form submissions and their status - - - - - - - - Submitted At - Respondent - Response Summary - Platform - Actions - - - - {responses.items.map((response) => ( - - - {format(new Date(response.submitted_at), "MMM d, yyyy HH:mm")} - - -
- {response.hidden?.name || 'Anonymous'} - {response.hidden?.email || 'No email'} -
-
- - - {getResponseSummary(response)} - - - - - {response.metadata.platform} - - - - - -
- ))} -
-
-
-
-
- ); -}; - -const calculateProductRelevanceInsights = (responses) => { - if (!responses?.items?.length) return null; - - const yesResponses = responses.items.filter(r => - r.answers?.some(a => a.type === 'boolean' && a.boolean === true) - ).length; - - const noResponses = responses.items.filter(r => - r.answers?.some(a => a.type === 'boolean' && a.boolean === false) - ).length; - - const total = yesResponses + noResponses; - const yesPercentage = Math.round((yesResponses / total) * 100) || 0; - - return { - metrics: [ - { name: 'Yes', value: yesResponses, color: "#10b981" }, - { name: 'No', value: noResponses, color: "#ef4444" }, - ], - summary: { - yes_percentage: yesPercentage, - total_responses: total - } - }; -}; - -const calculateWinbackInsights = (responses) => { - if (!responses?.items?.length) return null; - - // Get likelihood ratings - const likelihoodAnswers = responses.items - .map(r => r.answers?.find(a => a.type === 'number')) - .filter(Boolean) - .map(a => a.number); - - const averageLikelihood = likelihoodAnswers.length - ? Math.round((likelihoodAnswers.reduce((a, b) => a + b, 0) / likelihoodAnswers.length) * 10) / 10 - : 0; - - // Get reasons for not ordering - const reasonsMap = new Map(); - responses.items.forEach(response => { - const reasonsAnswer = response.answers?.find(a => a.type === 'choices'); - if (reasonsAnswer?.choices?.labels) { - reasonsAnswer.choices.labels.forEach(label => { - reasonsMap.set(label, (reasonsMap.get(label) || 0) + 1); - }); - } - }); - - const sortedReasons = Array.from(reasonsMap.entries()) - .sort(([, a], [, b]) => b - a) - .map(([label, count]) => ({ - name: label, - value: count, - color: "#8b5cf6" - })); - - return { - likelihood: { - ratings: [...Array(5)].map((_, i) => ({ - name: (i + 1).toString(), - value: likelihoodAnswers.filter(r => r === i + 1).length, - color: "#8b5cf6" - })), - average: averageLikelihood - }, - reasons: sortedReasons - }; -}; - const ResponseFeed = ({ responses, title, renderSummary }) => ( @@ -371,23 +111,31 @@ const ResponseFeed = ({ responses, title, renderSummary }) => ( -
+
{responses.items.map((response) => ( - -
-
-
- {response.hidden?.name || 'Anonymous'} -
-
- {format(new Date(response.submitted_at), "MMM d, yyyy HH:mm")} -
-
- {renderSummary(response)} +
+
+
+
+ + {response.hidden?.name || 'Anonymous'} + + {response.hidden?.email && ( + + ({response.hidden.email}) + + )}
+
+ {renderSummary(response)}
- +
))}
@@ -401,14 +149,20 @@ const ProductRelevanceFeed = ({ responses }) => ( title="Product Relevance Responses" renderSummary={(response) => { const answer = response.answers?.find(a => a.type === 'boolean'); + const textAnswer = response.answers?.find(a => a.type === 'text')?.text; + return ( -
- +
+ {answer?.boolean ? "Yes" : "No"} - {response.answers?.find(a => a.type === 'text')?.text && ( - - "{response.answers.find(a => a.type === 'text').text}" + {textAnswer && ( + + "{textAnswer}" )}
@@ -424,25 +178,39 @@ const WinbackFeed = ({ responses }) => ( renderSummary={(response) => { const likelihoodAnswer = response.answers?.find(a => a.type === 'number'); const reasonsAnswer = response.answers?.find(a => a.type === 'choices'); - const feedbackAnswer = response.answers?.find(a => a.type === 'text'); + const otherAnswer = response.answers?.find(a => a.type === 'text' && a.field.ref.includes('other')); + const feedbackAnswer = response.answers?.find(a => a.type === 'text' && !a.field.ref.includes('other')); return ( -
-
- = 4 ? "success" : "warning"}> - Rating: {likelihoodAnswer?.number}/5 +
+
+ + {likelihoodAnswer?.number}/5 + {(reasonsAnswer?.choices?.labels || []).map((label, idx) => ( + + {label} + + ))}
- {reasonsAnswer?.choices?.labels && ( + {otherAnswer?.text && (
- Reasons:{" "} - {reasonsAnswer.choices.labels.join(", ")} + Other:{" "} + {otherAnswer.text}
)} {feedbackAnswer?.text && (
- Feedback:{" "} - "{feedbackAnswer.text}" + {feedbackAnswer.text}
)}
@@ -454,10 +222,14 @@ const WinbackFeed = ({ responses }) => ( const renderProductRelevanceBar = (metrics) => ( - Product Relevance Results - - {metrics.productRelevance.yesPercentage}% Positive • {metrics.productRelevance.yesCount} Yes / {metrics.productRelevance.noCount} No - +
+ Were the suggested products in this email relevant to you? +
+ + {metrics.productRelevance.yesPercentage}% Positive + +
+
@@ -470,11 +242,12 @@ const renderProductRelevanceBar = (metrics) => ( }]} layout="vertical" stackOffset="expand" - margin={{ top: 10, right: 0, left: -20, bottom: 0 }} + margin={{ top: 0, right: 0, left: -20, bottom: 0 }} > { if (payload && payload.length) { const yesCount = payload[0].payload.yes; @@ -483,38 +256,50 @@ const renderProductRelevanceBar = (metrics) => ( const yesPercent = Math.round((yesCount / total) * 100); const noPercent = Math.round((noCount / total) * 100); return ( -
-
-
Yes: {yesCount} ({yesPercent}%)
-
No: {noCount} ({noPercent}%)
-
-
+ + +
+
+ Yes: + {yesCount} ({yesPercent}%) +
+
+ No: + {noCount} ({noPercent}%) +
+
+
+
); } return null; }} /> - - - + + + {metrics.productRelevance.yesPercentage}% + + +
+
+
Yes: {metrics.productRelevance.yesCount}
+
No: {metrics.productRelevance.noCount}
+
); const renderLikelihoodChart = (metrics, responses) => { - // Get likelihood distribution from responses const likelihoodCounts = [1, 2, 3, 4, 5].map(rating => ({ rating: rating.toString(), count: responses.items @@ -525,34 +310,46 @@ const renderLikelihoodChart = (metrics, responses) => { return ( - Return Likelihood - - Average Rating: {metrics.winback.averageRating}/5 • {responses.items.length} Total Responses - +
+ How likely are you to place another order with us? + + {metrics.winback.averageRating} + /5 avg + +
- { + return value === "1" ? "Not at all likely" : value === "5" ? "Extremely likely" : ""; + }} + textAnchor="middle" + interval={0} + height={50} /> + { if (payload && payload.length) { const { rating, count } = payload[0].payload; return (
-
- Rating {rating}: {count} responses +
+ {rating} Rating: {count} responses
); @@ -564,20 +361,16 @@ const renderLikelihoodChart = (metrics, responses) => { {likelihoodCounts.map((_, index) => ( = 3 ? "#10b981" : index === 2 ? "#f59e0b" : "#ef4444"} + fill={ + index === 0 ? "#ef4444" : // red + index === 1 ? "#f97316" : // orange + index === 2 ? "#eab308" : // yellow + index === 3 ? "#84cc16" : // lime + "#10b981" // green + } /> ))} -
@@ -586,88 +379,6 @@ const renderLikelihoodChart = (metrics, responses) => { ); }; -const MetricCard = ({ - title, - value, - delta, - suffix = "", - icon: Icon, - colorClass = "blue", - more_is_better = true, - loading = false, -}) => { - const getDeltaColor = (d) => { - if (d === 0) return "text-gray-600 dark:text-gray-400"; - const isPositive = d > 0; - return isPositive === more_is_better - ? "text-green-600 dark:text-green-500" - : "text-red-600 dark:text-red-500"; - }; - - const formatDelta = (d) => { - if (d === undefined || d === null) return null; - if (d === 0) return "0"; - return Math.abs(d) + suffix; - }; - - return ( - - -
-
- {loading ? ( - <> - -
- - -
- - ) : ( - <> -

{title}

-
-

- {typeof value === "number" - ? value.toLocaleString() + suffix - : value} -

- {delta !== undefined && delta !== 0 && ( -
- {delta > 0 ? ( - - ) : ( - - )} - - {formatDelta(delta)} - -
- )} -
- - )} -
- {!loading && Icon && ( - - )} - {loading && ( - - )} -
-
-
- ); -}; - -// Update the TypeformDashboard component to use pagination const TypeformDashboard = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -677,7 +388,7 @@ const TypeformDashboard = () => { }); const fetchResponses = async (formId, before = null) => { - const params = { page_size: 1000 }; // Start with max page size + const params = { page_size: 1000 }; if (before) params.before = before; const response = await axios.get(`/api/typeform/forms/${formId}/responses`, { params }); @@ -787,52 +498,56 @@ const TypeformDashboard = () => { } return ( -
- {loading ? ( -
- - -
- ) : ( - <> -
- {renderProductRelevanceBar(metrics)} - {renderLikelihoodChart(metrics, formData.form2.responses)} + + + {loading ? ( +
+ +
+ ) : ( + <> +
+ {renderProductRelevanceBar(metrics)} + {renderLikelihoodChart(metrics, formData.form2.responses)} +
- - - Reasons for Not Ordering - - - - - - Reason - Count - % - - - - {metrics.winback.reasons.map((reason, index) => ( - - {reason.reason} - {reason.count} - {reason.percentage}% - - ))} - -
-
-
- -
- - -
- - )} -
+
+
+ + + Reasons for Not Ordering + + + + + + Reason + Count + % + + + + {metrics.winback.reasons.map((reason, index) => ( + + {reason.reason} + {reason.count} + {reason.percentage}% + + ))} + +
+
+
+
+
+
+ +
+ + )} + + ); };