@@ -137,6 +137,9 @@ const DashboardLayout = () => {
+
+
+
diff --git a/dashboard/src/components/dashboard/Navigation.jsx b/dashboard/src/components/dashboard/Navigation.jsx
index e26d161..ef90f01 100644
--- a/dashboard/src/components/dashboard/Navigation.jsx
+++ b/dashboard/src/components/dashboard/Navigation.jsx
@@ -27,6 +27,7 @@ const Navigation = () => {
{ id: "analytics", label: "Analytics" },
{ id: "user-behavior", label: "User Behavior" },
{ id: "meta-campaigns", label: "Meta Ads" },
+ { id: "typeform", label: "Customer Surveys" },
{ id: "gorgias-overview", label: "Customer Service" },
{ id: "calls", label: "Calls" },
];
diff --git a/dashboard/src/components/dashboard/TypeformDashboard.jsx b/dashboard/src/components/dashboard/TypeformDashboard.jsx
index 63e81f1..dd35a9d 100644
--- a/dashboard/src/components/dashboard/TypeformDashboard.jsx
+++ b/dashboard/src/components/dashboard/TypeformDashboard.jsx
@@ -1,5 +1,5 @@
-import React, { useState, useEffect } from 'react';
-import axios from 'axios';
+import React, { useState, useEffect } from "react";
+import axios from "axios";
import {
Card,
CardContent,
@@ -30,7 +30,7 @@ import {
Tooltip,
ResponsiveContainer,
Cell,
- ReferenceLine
+ ReferenceLine,
} from "recharts";
// Get form IDs from environment variables
@@ -40,8 +40,8 @@ const FORM_IDS = {
};
const FORM_NAMES = {
- [FORM_IDS.FORM_1]: 'Product Relevance',
- [FORM_IDS.FORM_2]: 'Winback Survey',
+ [FORM_IDS.FORM_1]: "Product Relevance",
+ [FORM_IDS.FORM_2]: "Winback Survey",
};
// Loading skeleton components
@@ -128,28 +128,30 @@ const ProductRelevanceFeed = ({ responses }) => (
responses={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;
-
+ const answer = response.answers?.find((a) => a.type === "boolean");
+ const textAnswer = response.answers?.find((a) => a.type === "text")?.text;
+
return (
{textAnswer && (
-
- "{textAnswer}"
-
+
"{textAnswer}"
)}
);
@@ -178,34 +178,43 @@ const WinbackFeed = ({ responses }) => (
responses={responses}
title="Winback Survey 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' && a.field.type === 'long_text');
-
+ 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" && a.field.type === "long_text"
+ );
+
return (
{response.hidden?.email ? (
-
- {response.hidden?.name || 'Anonymous'}
+ {response.hidden?.name || "Anonymous"}
) : (
- {response.hidden?.name || 'Anonymous'}
+ {response.hidden?.name || "Anonymous"}
)}
-
{likelihoodAnswer?.number}/5
@@ -246,14 +255,17 @@ const TypeformDashboard = () => {
const [error, setError] = useState(null);
const [formData, setFormData] = useState({
form1: { responses: null, hasMore: false, lastToken: null },
- form2: { responses: null, hasMore: false, lastToken: null }
+ form2: { responses: null, hasMore: false, lastToken: null },
});
const fetchResponses = async (formId, before = null) => {
const params = { page_size: 1000 };
if (before) params.before = before;
-
- const response = await axios.get(`/api/typeform/forms/${formId}/responses`, { params });
+
+ const response = await axios.get(
+ `/api/typeform/forms/${formId}/responses`,
+ { params }
+ );
return response.data;
};
@@ -268,23 +280,25 @@ const TypeformDashboard = () => {
forms.map(async (formId) => {
const responses = await fetchResponses(formId);
const hasMore = responses.items.length === 1000;
- const lastToken = hasMore ? responses.items[responses.items.length - 1].token : null;
-
+ const lastToken = hasMore
+ ? responses.items[responses.items.length - 1].token
+ : null;
+
return {
responses,
hasMore,
- lastToken
+ lastToken,
};
})
);
setFormData({
form1: results[0],
- form2: results[1]
+ form2: results[1],
});
} catch (err) {
- console.error('Error fetching Typeform data:', err);
- setError('Failed to load form data. Please try again later.');
+ console.error("Error fetching Typeform data:", err);
+ setError("Failed to load form data. Please try again later.");
} finally {
setLoading(false);
}
@@ -300,27 +314,31 @@ const TypeformDashboard = () => {
const form2Responses = formData.form2.responses.items;
// Product Relevance metrics
- const yesResponses = form1Responses.filter(r =>
- r.answers?.some(a => a.type === 'boolean' && a.boolean === true)
+ const yesResponses = form1Responses.filter((r) =>
+ r.answers?.some((a) => a.type === "boolean" && a.boolean === true)
).length;
const totalForm1 = form1Responses.length;
const yesPercentage = Math.round((yesResponses / totalForm1) * 100) || 0;
// Winback Survey metrics
const likelihoodAnswers = form2Responses
- .map(r => r.answers?.find(a => a.type === 'number'))
+ .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
+ .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 (only predefined choices)
const reasonsMap = new Map();
- form2Responses.forEach(response => {
- const reasonsAnswer = response.answers?.find(a => a.type === 'choices');
+ form2Responses.forEach((response) => {
+ const reasonsAnswer = response.answers?.find((a) => a.type === "choices");
if (reasonsAnswer?.choices?.labels) {
- reasonsAnswer.choices.labels.forEach(label => {
+ reasonsAnswer.choices.labels.forEach((label) => {
reasonsMap.set(label, (reasonsMap.get(label) || 0) + 1);
});
}
@@ -331,19 +349,19 @@ const TypeformDashboard = () => {
.map(([label, count]) => ({
reason: label,
count,
- percentage: Math.round((count / form2Responses.length) * 100)
+ percentage: Math.round((count / form2Responses.length) * 100),
}));
return {
productRelevance: {
yesPercentage,
yesCount: yesResponses,
- noCount: totalForm1 - yesResponses
+ noCount: totalForm1 - yesResponses,
},
winback: {
averageRating: averageLikelihood,
- reasons: sortedReasons
- }
+ reasons: sortedReasons,
+ },
};
};
@@ -351,15 +369,21 @@ const TypeformDashboard = () => {
// Find the newest response across both forms
const getNewestResponse = () => {
- if (!formData.form1.responses?.items?.length && !formData.form2.responses?.items?.length) return null;
-
+ if (
+ !formData.form1.responses?.items?.length &&
+ !formData.form2.responses?.items?.length
+ )
+ return null;
+
const form1Latest = formData.form1.responses?.items[0]?.submitted_at;
const form2Latest = formData.form2.responses?.items[0]?.submitted_at;
-
+
if (!form1Latest) return form2Latest;
if (!form2Latest) return form1Latest;
-
- return new Date(form1Latest) > new Date(form2Latest) ? form1Latest : form2Latest;
+
+ return new Date(form1Latest) > new Date(form2Latest)
+ ? form1Latest
+ : form2Latest;
};
const newestResponse = getNewestResponse();
@@ -377,12 +401,16 @@ const TypeformDashboard = () => {
}
// Calculate likelihood counts for the chart
- const likelihoodCounts = !loading && formData.form2.responses ? [1, 2, 3, 4, 5].map(rating => ({
- rating: rating.toString(),
- count: formData.form2.responses.items
- .filter(r => r.answers?.find(a => a.type === 'number')?.number === rating)
- .length
- })) : [];
+ const likelihoodCounts =
+ !loading && formData.form2.responses
+ ? [1, 2, 3, 4, 5].map((rating) => ({
+ rating: rating.toString(),
+ count: formData.form2.responses.items.filter(
+ (r) =>
+ r.answers?.find((a) => a.type === "number")?.number === rating
+ ).length,
+ }))
+ : [];
return (
@@ -393,7 +421,8 @@ const TypeformDashboard = () => {
{newestResponse && (
- Newest response: {format(new Date(newestResponse), "MMM d, h:mm a")}
+ Newest response:{" "}
+ {format(new Date(newestResponse), "MMM d, h:mm a")}
)}
@@ -407,36 +436,51 @@ const TypeformDashboard = () => {
) : (
<>
-
-
- How likely are you to place another order with us?
-
+
+ How likely are you to place another order with us?
+
+
{metrics.winback.averageRating}
- /5 avg
+
+ /5 avg
+
-
-
+
{
- return value === "1" ? "Not at all" : value === "5" ? "Extremely" : "";
+ return value === "1"
+ ? "Not at all"
+ : value === "5"
+ ? "Extremely"
+ : "";
}}
textAnchor="middle"
interval={0}
@@ -463,14 +507,18 @@ const TypeformDashboard = () => {
/>
{likelihoodCounts.map((_, index) => (
- |
))}
@@ -483,7 +531,9 @@ const TypeformDashboard = () => {
-
Were the suggested products in this email relevant to you?
+
+ Were the suggested products in this email relevant to you?
+
{metrics.productRelevance.yesPercentage}% Relevant
@@ -495,11 +545,15 @@ const TypeformDashboard = () => {
{
const yesCount = payload[0].payload.yes;
const noCount = payload[0].payload.no;
const total = yesCount + noCount;
- const yesPercent = Math.round((yesCount / total) * 100);
- const noPercent = Math.round((noCount / total) * 100);
+ const yesPercent = Math.round(
+ (yesCount / total) * 100
+ );
+ const noPercent = Math.round(
+ (noCount / total) * 100
+ );
return (
- Yes:
- {yesCount} ({yesPercent}%)
+
+ Yes:
+
+
+ {yesCount} ({yesPercent}%)
+
- No:
- {noCount} ({noPercent}%)
+
+ No:
+
+
+ {noCount} ({noPercent}%)
+
@@ -535,7 +601,12 @@ const TypeformDashboard = () => {
return null;
}}
/>
-
+
{
{metrics.productRelevance.yesPercentage}%
-
+
@@ -561,26 +637,43 @@ const TypeformDashboard = () => {
-
+
- Reasons for Not Ordering
+
+ Reasons for Not Ordering
+
- Reason
- Count
- %
+
+ Reason
+
+
+ Count
+
+
+ %
+
{metrics.winback.reasons.map((reason, index) => (
-
- {reason.reason}
- {reason.count}
- {reason.percentage}%
+
+
+ {reason.reason}
+
+
+ {reason.count}
+
+
+ {reason.percentage}%
+
))}
@@ -589,7 +682,7 @@ const TypeformDashboard = () => {
-
+
@@ -604,4 +697,4 @@ const TypeformDashboard = () => {
);
};
-export default TypeformDashboard;
\ No newline at end of file
+export default TypeformDashboard;