diff --git a/dashboard/src/App.jsx b/dashboard/src/App.jsx index 3fb256e..d6bc85d 100644 --- a/dashboard/src/App.jsx +++ b/dashboard/src/App.jsx @@ -95,7 +95,7 @@ const DashboardLayout = () => {
- +
@@ -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 (
{response.hidden?.email ? ( - - {response.hidden?.name || 'Anonymous'} + {response.hidden?.name || "Anonymous"} ) : ( - {response.hidden?.name || 'Anonymous'} + {response.hidden?.name || "Anonymous"} )} - {answer?.boolean ? "Yes" : "No"} @@ -163,9 +165,7 @@ const ProductRelevanceFeed = ({ responses }) => (
{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;