Integrate typeform into app
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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,8 +128,8 @@ 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">
|
||||||
@@ -140,16 +140,18 @@ const ProductRelevanceFeed = ({ responses }) => (
|
|||||||
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,9 +178,13 @@ 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">
|
||||||
@@ -191,21 +195,26 @@ const WinbackFeed = ({ responses }) => (
|
|||||||
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,7 +369,11 @@ 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;
|
||||||
@@ -359,7 +381,9 @@ const TypeformDashboard = () => {
|
|||||||
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,21 +436,29 @@ 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>
|
||||||
@@ -432,11 +469,18 @@ const TypeformDashboard = () => {
|
|||||||
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}
|
||||||
@@ -466,11 +510,15 @@ const TypeformDashboard = () => {
|
|||||||
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user