Integrate typeform into app

This commit is contained in:
2024-12-30 13:48:00 -05:00
parent 3dbf3ee127
commit ef0d48826a
3 changed files with 207 additions and 110 deletions

View File

@@ -95,7 +95,7 @@ const DashboardLayout = () => {
<Navigation /> <Navigation />
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<TypeformDashboard />
<div className="grid grid-cols-1 xl:grid-cols-6 gap-4"> <div className="grid grid-cols-1 xl:grid-cols-6 gap-4">
<div className="xl:col-span-4 col-span-6"> <div className="xl:col-span-4 col-span-6">
<div className="space-y-4 h-full w-full"> <div className="space-y-4 h-full w-full">
@@ -137,6 +137,9 @@ const DashboardLayout = () => {
<div id="meta-campaigns"> <div id="meta-campaigns">
<MetaCampaigns /> <MetaCampaigns />
</div> </div>
<div id="typeform">
<TypeformDashboard />
</div>
<div id="gorgias-overview"> <div id="gorgias-overview">
<GorgiasOverview /> <GorgiasOverview />
</div> </div>

View File

@@ -27,6 +27,7 @@ const Navigation = () => {
{ id: "analytics", label: "Analytics" }, { id: "analytics", label: "Analytics" },
{ id: "user-behavior", label: "User Behavior" }, { id: "user-behavior", label: "User Behavior" },
{ id: "meta-campaigns", label: "Meta Ads" }, { id: "meta-campaigns", label: "Meta Ads" },
{ id: "typeform", label: "Customer Surveys" },
{ id: "gorgias-overview", label: "Customer Service" }, { id: "gorgias-overview", label: "Customer Service" },
{ id: "calls", label: "Calls" }, { id: "calls", label: "Calls" },
]; ];

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import axios from 'axios'; import axios from "axios";
import { import {
Card, Card,
CardContent, CardContent,
@@ -30,7 +30,7 @@ import {
Tooltip, Tooltip,
ResponsiveContainer, ResponsiveContainer,
Cell, Cell,
ReferenceLine ReferenceLine,
} from "recharts"; } from "recharts";
// Get form IDs from environment variables // Get form IDs from environment variables
@@ -40,8 +40,8 @@ const FORM_IDS = {
}; };
const FORM_NAMES = { const FORM_NAMES = {
[FORM_IDS.FORM_1]: 'Product Relevance', [FORM_IDS.FORM_1]: "Product Relevance",
[FORM_IDS.FORM_2]: 'Winback Survey', [FORM_IDS.FORM_2]: "Winback Survey",
}; };
// Loading skeleton components // Loading skeleton components
@@ -128,28 +128,30 @@ const ProductRelevanceFeed = ({ responses }) => (
responses={responses} responses={responses}
title="Product Relevance Responses" title="Product Relevance Responses"
renderSummary={(response) => { renderSummary={(response) => {
const answer = response.answers?.find(a => a.type === 'boolean'); const answer = response.answers?.find((a) => a.type === "boolean");
const textAnswer = response.answers?.find(a => a.type === 'text')?.text; const textAnswer = response.answers?.find((a) => a.type === "text")?.text;
return ( return (
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{response.hidden?.email ? ( {response.hidden?.email ? (
<a <a
href={`https://backend.acherryontop.com/search/?search_for=customers&search=${response.hidden.email}`} href={`https://backend.acherryontop.com/search/?search_for=customers&search=${response.hidden.email}`}
className="text-sm font-medium text-gray-900 dark:text-gray-100 hover:underline" className="text-sm font-medium text-gray-900 dark:text-gray-100 hover:underline"
> >
{response.hidden?.name || 'Anonymous'} {response.hidden?.name || "Anonymous"}
</a> </a>
) : ( ) : (
<span className="text-sm font-medium text-gray-900 dark:text-gray-100"> <span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{response.hidden?.name || 'Anonymous'} {response.hidden?.name || "Anonymous"}
</span> </span>
)} )}
<Badge <Badge
className={ className={
answer?.boolean ? "bg-green-200 text-green-700" : "bg-red-200 text-red-700" answer?.boolean
? "bg-green-200 text-green-700"
: "bg-red-200 text-red-700"
} }
> >
{answer?.boolean ? "Yes" : "No"} {answer?.boolean ? "Yes" : "No"}
@@ -163,9 +165,7 @@ const ProductRelevanceFeed = ({ responses }) => (
</time> </time>
</div> </div>
{textAnswer && ( {textAnswer && (
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">"{textAnswer}"</div>
"{textAnswer}"
</div>
)} )}
</div> </div>
); );
@@ -178,34 +178,43 @@ const WinbackFeed = ({ responses }) => (
responses={responses} responses={responses}
title="Winback Survey Responses" title="Winback Survey Responses"
renderSummary={(response) => { renderSummary={(response) => {
const likelihoodAnswer = response.answers?.find(a => a.type === 'number'); const likelihoodAnswer = response.answers?.find(
const reasonsAnswer = response.answers?.find(a => a.type === 'choices'); (a) => a.type === "number"
const feedbackAnswer = response.answers?.find(a => a.type === 'text' && a.field.type === 'long_text'); );
const reasonsAnswer = response.answers?.find((a) => a.type === "choices");
const feedbackAnswer = response.answers?.find(
(a) => a.type === "text" && a.field.type === "long_text"
);
return ( return (
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{response.hidden?.email ? ( {response.hidden?.email ? (
<a <a
href={`https://backend.acherryontop.com/search/?search_for=customers&search=${response.hidden.email}`} href={`https://backend.acherryontop.com/search/?search_for=customers&search=${response.hidden.email}`}
className="text-sm font-medium text-gray-900 dark:text-gray-100 hover:underline" className="text-sm font-medium text-gray-900 dark:text-gray-100 hover:underline"
> >
{response.hidden?.name || 'Anonymous'} {response.hidden?.name || "Anonymous"}
</a> </a>
) : ( ) : (
<span className="text-sm font-medium text-gray-900 dark:text-gray-100"> <span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{response.hidden?.name || 'Anonymous'} {response.hidden?.name || "Anonymous"}
</span> </span>
)} )}
<Badge <Badge
className={ className={
likelihoodAnswer?.number === 1 ? "bg-red-200 text-red-700" : likelihoodAnswer?.number === 1
likelihoodAnswer?.number === 2 ? "bg-orange-200 text-orange-700" : ? "bg-red-200 text-red-700"
likelihoodAnswer?.number === 3 ? "bg-yellow-200 text-yellow-700" : : likelihoodAnswer?.number === 2
likelihoodAnswer?.number === 4 ? "bg-lime-200 text-lime-700" : ? "bg-orange-200 text-orange-700"
likelihoodAnswer?.number === 5 ? "bg-green-200 text-green-700" : : likelihoodAnswer?.number === 3
"bg-gray-200 text-gray-700" ? "bg-yellow-200 text-yellow-700"
: likelihoodAnswer?.number === 4
? "bg-lime-200 text-lime-700"
: likelihoodAnswer?.number === 5
? "bg-green-200 text-green-700"
: "bg-gray-200 text-gray-700"
} }
> >
{likelihoodAnswer?.number}/5 {likelihoodAnswer?.number}/5
@@ -246,14 +255,17 @@ const TypeformDashboard = () => {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
form1: { responses: null, hasMore: false, lastToken: null }, 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 fetchResponses = async (formId, before = null) => {
const params = { page_size: 1000 }; const params = { page_size: 1000 };
if (before) params.before = before; 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; return response.data;
}; };
@@ -268,23 +280,25 @@ const TypeformDashboard = () => {
forms.map(async (formId) => { forms.map(async (formId) => {
const responses = await fetchResponses(formId); const responses = await fetchResponses(formId);
const hasMore = responses.items.length === 1000; 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 { return {
responses, responses,
hasMore, hasMore,
lastToken lastToken,
}; };
}) })
); );
setFormData({ setFormData({
form1: results[0], form1: results[0],
form2: results[1] form2: results[1],
}); });
} catch (err) { } catch (err) {
console.error('Error fetching Typeform data:', err); console.error("Error fetching Typeform data:", err);
setError('Failed to load form data. Please try again later.'); setError("Failed to load form data. Please try again later.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -300,27 +314,31 @@ const TypeformDashboard = () => {
const form2Responses = formData.form2.responses.items; const form2Responses = formData.form2.responses.items;
// Product Relevance metrics // Product Relevance metrics
const yesResponses = form1Responses.filter(r => const yesResponses = form1Responses.filter((r) =>
r.answers?.some(a => a.type === 'boolean' && a.boolean === true) r.answers?.some((a) => a.type === "boolean" && a.boolean === true)
).length; ).length;
const totalForm1 = form1Responses.length; const totalForm1 = form1Responses.length;
const yesPercentage = Math.round((yesResponses / totalForm1) * 100) || 0; const yesPercentage = Math.round((yesResponses / totalForm1) * 100) || 0;
// Winback Survey metrics // Winback Survey metrics
const likelihoodAnswers = form2Responses const likelihoodAnswers = form2Responses
.map(r => r.answers?.find(a => a.type === 'number')) .map((r) => r.answers?.find((a) => a.type === "number"))
.filter(Boolean) .filter(Boolean)
.map(a => a.number); .map((a) => a.number);
const averageLikelihood = likelihoodAnswers.length const averageLikelihood = likelihoodAnswers.length
? Math.round((likelihoodAnswers.reduce((a, b) => a + b, 0) / likelihoodAnswers.length) * 10) / 10 ? Math.round(
(likelihoodAnswers.reduce((a, b) => a + b, 0) /
likelihoodAnswers.length) *
10
) / 10
: 0; : 0;
// Get reasons for not ordering (only predefined choices) // Get reasons for not ordering (only predefined choices)
const reasonsMap = new Map(); const reasonsMap = new Map();
form2Responses.forEach(response => { form2Responses.forEach((response) => {
const reasonsAnswer = response.answers?.find(a => a.type === 'choices'); const reasonsAnswer = response.answers?.find((a) => a.type === "choices");
if (reasonsAnswer?.choices?.labels) { if (reasonsAnswer?.choices?.labels) {
reasonsAnswer.choices.labels.forEach(label => { reasonsAnswer.choices.labels.forEach((label) => {
reasonsMap.set(label, (reasonsMap.get(label) || 0) + 1); reasonsMap.set(label, (reasonsMap.get(label) || 0) + 1);
}); });
} }
@@ -331,19 +349,19 @@ const TypeformDashboard = () => {
.map(([label, count]) => ({ .map(([label, count]) => ({
reason: label, reason: label,
count, count,
percentage: Math.round((count / form2Responses.length) * 100) percentage: Math.round((count / form2Responses.length) * 100),
})); }));
return { return {
productRelevance: { productRelevance: {
yesPercentage, yesPercentage,
yesCount: yesResponses, yesCount: yesResponses,
noCount: totalForm1 - yesResponses noCount: totalForm1 - yesResponses,
}, },
winback: { winback: {
averageRating: averageLikelihood, averageRating: averageLikelihood,
reasons: sortedReasons reasons: sortedReasons,
} },
}; };
}; };
@@ -351,15 +369,21 @@ const TypeformDashboard = () => {
// Find the newest response across both forms // Find the newest response across both forms
const getNewestResponse = () => { 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 form1Latest = formData.form1.responses?.items[0]?.submitted_at;
const form2Latest = formData.form2.responses?.items[0]?.submitted_at; const form2Latest = formData.form2.responses?.items[0]?.submitted_at;
if (!form1Latest) return form2Latest; if (!form1Latest) return form2Latest;
if (!form2Latest) return form1Latest; 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(); const newestResponse = getNewestResponse();
@@ -377,12 +401,16 @@ const TypeformDashboard = () => {
} }
// Calculate likelihood counts for the chart // Calculate likelihood counts for the chart
const likelihoodCounts = !loading && formData.form2.responses ? [1, 2, 3, 4, 5].map(rating => ({ const likelihoodCounts =
rating: rating.toString(), !loading && formData.form2.responses
count: formData.form2.responses.items ? [1, 2, 3, 4, 5].map((rating) => ({
.filter(r => r.answers?.find(a => a.type === 'number')?.number === rating) rating: rating.toString(),
.length count: formData.form2.responses.items.filter(
})) : []; (r) =>
r.answers?.find((a) => a.type === "number")?.number === rating
).length,
}))
: [];
return ( return (
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm"> <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
@@ -393,7 +421,8 @@ const TypeformDashboard = () => {
</CardTitle> </CardTitle>
{newestResponse && ( {newestResponse && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Newest response: {format(new Date(newestResponse), "MMM d, h:mm a")} Newest response:{" "}
{format(new Date(newestResponse), "MMM d, h:mm a")}
</p> </p>
)} )}
</div> </div>
@@ -407,36 +436,51 @@ const TypeformDashboard = () => {
) : ( ) : (
<> <>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-6">
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm"> <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="p-6"> <CardHeader className="p-6">
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">How likely are you to place another order with us?</CardTitle> <CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">
<span className={`text-2xl font-bold ${ How likely are you to place another order with us?
metrics.winback.averageRating <= 1 ? "text-red-600 dark:text-red-500" : </CardTitle>
metrics.winback.averageRating <= 2 ? "text-orange-600 dark:text-orange-500" : <span
metrics.winback.averageRating <= 3 ? "text-yellow-600 dark:text-yellow-500" : className={`text-2xl font-bold ${
metrics.winback.averageRating <= 4 ? "text-lime-600 dark:text-lime-500" : metrics.winback.averageRating <= 1
"text-green-600 dark:text-green-500" ? "text-red-600 dark:text-red-500"
}`}> : metrics.winback.averageRating <= 2
? "text-orange-600 dark:text-orange-500"
: metrics.winback.averageRating <= 3
? "text-yellow-600 dark:text-yellow-500"
: metrics.winback.averageRating <= 4
? "text-lime-600 dark:text-lime-500"
: "text-green-600 dark:text-green-500"
}`}
>
{metrics.winback.averageRating} {metrics.winback.averageRating}
<span className="text-base font-normal text-muted-foreground">/5 avg</span> <span className="text-base font-normal text-muted-foreground">
/5 avg
</span>
</span> </span>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="h-[200px]"> <div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart <BarChart
data={likelihoodCounts} data={likelihoodCounts}
margin={{ top: 0, right: 10, left: -20, bottom: -25 }} margin={{ top: 0, right: 10, left: -20, bottom: -25 }}
> >
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" /> <CartesianGrid
strokeDasharray="3 3"
className="stroke-muted"
/>
<XAxis <XAxis
dataKey="rating" dataKey="rating"
tickFormatter={(value) => { tickFormatter={(value) => {
return value === "1" ? "Not at all" : value === "5" ? "Extremely" : ""; return value === "1"
? "Not at all"
: value === "5"
? "Extremely"
: "";
}} }}
textAnchor="middle" textAnchor="middle"
interval={0} interval={0}
@@ -463,14 +507,18 @@ const TypeformDashboard = () => {
/> />
<Bar dataKey="count"> <Bar dataKey="count">
{likelihoodCounts.map((_, index) => ( {likelihoodCounts.map((_, index) => (
<Cell <Cell
key={`cell-${index}`} key={`cell-${index}`}
fill={ fill={
index === 0 ? "#ef4444" : // red index === 0
index === 1 ? "#f97316" : // orange ? "#ef4444" // red
index === 2 ? "#eab308" : // yellow : index === 1
index === 3 ? "#84cc16" : // lime ? "#f97316" // orange
"#10b981" // green : index === 2
? "#eab308" // yellow
: index === 3
? "#84cc16" // lime
: "#10b981" // green
} }
/> />
))} ))}
@@ -483,7 +531,9 @@ const TypeformDashboard = () => {
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm"> <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="p-6"> <CardHeader className="p-6">
<div className="flex items-baseline justify-between gap-2"> <div className="flex items-baseline justify-between gap-2">
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Were the suggested products in this email relevant to you?</CardTitle> <CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Were the suggested products in this email relevant to you?
</CardTitle>
<div className="flex flex-col items-end"> <div className="flex flex-col items-end">
<span className="text-2xl font-bold text-green-600 dark:text-green-500"> <span className="text-2xl font-bold text-green-600 dark:text-green-500">
{metrics.productRelevance.yesPercentage}% Relevant {metrics.productRelevance.yesPercentage}% Relevant
@@ -495,11 +545,15 @@ const TypeformDashboard = () => {
<div className="h-[100px]"> <div className="h-[100px]">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart <BarChart
data={[{ data={[
yes: metrics.productRelevance.yesCount, {
no: metrics.productRelevance.noCount, yes: metrics.productRelevance.yesCount,
total: metrics.productRelevance.yesCount + metrics.productRelevance.noCount no: metrics.productRelevance.noCount,
}]} total:
metrics.productRelevance.yesCount +
metrics.productRelevance.noCount,
},
]}
layout="vertical" layout="vertical"
stackOffset="expand" stackOffset="expand"
margin={{ top: 0, right: 0, left: -20, bottom: 0 }} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}
@@ -513,19 +567,31 @@ const TypeformDashboard = () => {
const yesCount = payload[0].payload.yes; const yesCount = payload[0].payload.yes;
const noCount = payload[0].payload.no; const noCount = payload[0].payload.no;
const total = yesCount + noCount; const total = yesCount + noCount;
const yesPercent = Math.round((yesCount / total) * 100); const yesPercent = Math.round(
const noPercent = Math.round((noCount / total) * 100); (yesCount / total) * 100
);
const noPercent = Math.round(
(noCount / total) * 100
);
return ( return (
<Card className="p-3 shadow-lg bg-white dark:bg-gray-900/60 backdrop-blur-sm border-none"> <Card className="p-3 shadow-lg bg-white dark:bg-gray-900/60 backdrop-blur-sm border-none">
<CardContent className="p-0 space-y-2"> <CardContent className="p-0 space-y-2">
<div className="space-y-1"> <div className="space-y-1">
<div className="flex justify-between items-center text-sm"> <div className="flex justify-between items-center text-sm">
<span className="text-emerald-500 font-medium">Yes:</span> <span className="text-emerald-500 font-medium">
<span className="ml-4 text-muted-foreground">{yesCount} ({yesPercent}%)</span> Yes:
</span>
<span className="ml-4 text-muted-foreground">
{yesCount} ({yesPercent}%)
</span>
</div> </div>
<div className="flex justify-between items-center text-sm"> <div className="flex justify-between items-center text-sm">
<span className="text-red-500 font-medium">No:</span> <span className="text-red-500 font-medium">
<span className="ml-4 text-muted-foreground">{noCount} ({noPercent}%)</span> No:
</span>
<span className="ml-4 text-muted-foreground">
{noCount} ({noPercent}%)
</span>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -535,7 +601,12 @@ const TypeformDashboard = () => {
return null; return null;
}} }}
/> />
<Bar dataKey="yes" stackId="stack" fill="#10b981" radius={[0, 0, 0, 0]}> <Bar
dataKey="yes"
stackId="stack"
fill="#10b981"
radius={[0, 0, 0, 0]}
>
<text <text
x="50%" x="50%"
y="50%" y="50%"
@@ -547,7 +618,12 @@ const TypeformDashboard = () => {
{metrics.productRelevance.yesPercentage}% {metrics.productRelevance.yesPercentage}%
</text> </text>
</Bar> </Bar>
<Bar dataKey="no" stackId="stack" fill="#ef4444" radius={[0, 0, 0, 0]} /> <Bar
dataKey="no"
stackId="stack"
fill="#ef4444"
radius={[0, 0, 0, 0]}
/>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@@ -561,26 +637,43 @@ const TypeformDashboard = () => {
<div className="grid grid-cols-2 lg:grid-cols-12 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-12 gap-4">
<div className="col-span-4 lg:col-span-12 xl:col-span-4"> <div className="col-span-4 lg:col-span-12 xl:col-span-4">
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm"> <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm h-full">
<CardHeader> <CardHeader>
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Reasons for Not Ordering</CardTitle> <CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Reasons for Not Ordering
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600"> <div className="overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="font-medium text-gray-900 dark:text-gray-100">Reason</TableHead> <TableHead className="font-medium text-gray-900 dark:text-gray-100">
<TableHead className="text-right font-medium text-gray-900 dark:text-gray-100">Count</TableHead> Reason
<TableHead className="text-right w-[80px] font-medium text-gray-900 dark:text-gray-100">%</TableHead> </TableHead>
<TableHead className="text-right font-medium text-gray-900 dark:text-gray-100">
Count
</TableHead>
<TableHead className="text-right w-[80px] font-medium text-gray-900 dark:text-gray-100">
%
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{metrics.winback.reasons.map((reason, index) => ( {metrics.winback.reasons.map((reason, index) => (
<TableRow key={index} className="hover:bg-muted/50 transition-colors"> <TableRow
<TableCell className="font-medium text-gray-900 dark:text-gray-100">{reason.reason}</TableCell> key={index}
<TableCell className="text-right text-muted-foreground">{reason.count}</TableCell> className="hover:bg-muted/50 transition-colors"
<TableCell className="text-right text-muted-foreground">{reason.percentage}%</TableCell> >
<TableCell className="font-medium text-gray-900 dark:text-gray-100">
{reason.reason}
</TableCell>
<TableCell className="text-right text-muted-foreground">
{reason.count}
</TableCell>
<TableCell className="text-right text-muted-foreground">
{reason.percentage}%
</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -589,7 +682,7 @@ const TypeformDashboard = () => {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<div className="col-span-4 lg:col-span-6 xl:col-span-4"> <div className="col-span-4 lg:col-span-6 xl:col-span-4">
<WinbackFeed responses={formData.form2.responses} /> <WinbackFeed responses={formData.form2.responses} />
</div> </div>
@@ -604,4 +697,4 @@ const TypeformDashboard = () => {
); );
}; };
export default TypeformDashboard; export default TypeformDashboard;