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 }) => (
-
-);
-
-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}%
+
+ ))}
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
);
};