Standardize typeform and rearrange
This commit is contained in:
@@ -46,7 +46,7 @@ const FORM_NAMES = {
|
|||||||
|
|
||||||
// Loading skeleton components
|
// Loading skeleton components
|
||||||
const SkeletonChart = () => (
|
const SkeletonChart = () => (
|
||||||
<div className="h-[300px] w-full bg-card rounded-lg p-6">
|
<div className="h-[300px] w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-6">
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
@@ -219,166 +219,6 @@ const WinbackFeed = ({ responses }) => (
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderProductRelevanceBar = (metrics) => (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-baseline justify-between">
|
|
||||||
<CardTitle className="text-lg font-semibold">Were the suggested products in this email relevant to you?</CardTitle>
|
|
||||||
<div className="flex flex-col items-end">
|
|
||||||
<span className="text-2xl font-bold text-green-600">
|
|
||||||
{metrics.productRelevance.yesPercentage}% Positive
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="h-[100px]">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<BarChart
|
|
||||||
data={[{
|
|
||||||
yes: metrics.productRelevance.yesCount,
|
|
||||||
no: metrics.productRelevance.noCount,
|
|
||||||
total: metrics.productRelevance.yesCount + metrics.productRelevance.noCount
|
|
||||||
}]}
|
|
||||||
layout="vertical"
|
|
||||||
stackOffset="expand"
|
|
||||||
margin={{ top: 0, right: 0, left: -20, bottom: 0 }}
|
|
||||||
>
|
|
||||||
<XAxis type="number" hide domain={[0, 1]} />
|
|
||||||
<YAxis type="category" hide />
|
|
||||||
<Tooltip
|
|
||||||
cursor={false}
|
|
||||||
content={({ payload }) => {
|
|
||||||
if (payload && payload.length) {
|
|
||||||
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);
|
|
||||||
return (
|
|
||||||
<Card className="p-3 shadow-lg bg-white dark:bg-gray-800 border-none">
|
|
||||||
<CardContent className="p-0 space-y-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex justify-between items-center text-sm">
|
|
||||||
<span className="text-emerald-500 font-medium">Yes:</span>
|
|
||||||
<span className="ml-4">{yesCount} ({yesPercent}%)</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center text-sm">
|
|
||||||
<span className="text-red-500 font-medium">No:</span>
|
|
||||||
<span className="ml-4">{noCount} ({noPercent}%)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Bar dataKey="yes" stackId="stack" fill="#10b981" radius={[4, 4, 0, 0]}>
|
|
||||||
<text
|
|
||||||
x="50%"
|
|
||||||
y="50%"
|
|
||||||
textAnchor="middle"
|
|
||||||
fill="#fff"
|
|
||||||
fontSize={14}
|
|
||||||
fontWeight="bold"
|
|
||||||
>
|
|
||||||
{metrics.productRelevance.yesPercentage}%
|
|
||||||
</text>
|
|
||||||
</Bar>
|
|
||||||
<Bar dataKey="no" stackId="stack" fill="#ef4444" radius={[0, 0, 4, 4]} />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-2 text-md font-semibold mx-1 text-muted-foreground">
|
|
||||||
<div>Yes: {metrics.productRelevance.yesCount}</div>
|
|
||||||
<div>No: {metrics.productRelevance.noCount}</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderLikelihoodChart = (metrics, responses) => {
|
|
||||||
const likelihoodCounts = [1, 2, 3, 4, 5].map(rating => ({
|
|
||||||
rating: rating.toString(),
|
|
||||||
count: responses.items
|
|
||||||
.filter(r => r.answers?.find(a => a.type === 'number')?.number === rating)
|
|
||||||
.length
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-baseline justify-between">
|
|
||||||
<CardTitle className="text-lg font-semibold">How likely are you to place another order with us?</CardTitle>
|
|
||||||
<span className={`text-2xl font-bold ${
|
|
||||||
metrics.winback.averageRating <= 1 ? "text-red-700" :
|
|
||||||
metrics.winback.averageRating <= 2 ? "text-orange-700" :
|
|
||||||
metrics.winback.averageRating <= 3 ? "text-yellow-700" :
|
|
||||||
metrics.winback.averageRating <= 4 ? "text-lime-700" :
|
|
||||||
"text-green-700"
|
|
||||||
}`}>
|
|
||||||
{metrics.winback.averageRating}
|
|
||||||
<span className="text-base font-normal text-muted-foreground">/5 avg</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="h-[200px]">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<BarChart
|
|
||||||
data={likelihoodCounts}
|
|
||||||
margin={{ top: 0, right: 0, left: -20, bottom: 0 }}
|
|
||||||
>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
||||||
<XAxis
|
|
||||||
dataKey="rating"
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
return value === "1" ? "Not at all likely" : value === "5" ? "Extremely likely" : "";
|
|
||||||
}}
|
|
||||||
textAnchor="middle"
|
|
||||||
interval={0}
|
|
||||||
height={50}
|
|
||||||
/>
|
|
||||||
<YAxis />
|
|
||||||
<Tooltip
|
|
||||||
content={({ payload }) => {
|
|
||||||
if (payload && payload.length) {
|
|
||||||
const { rating, count } = payload[0].payload;
|
|
||||||
return (
|
|
||||||
<div className="bg-white dark:bg-gray-800 p-2 rounded-md shadow-md border">
|
|
||||||
<div className="text-sm font-medium">
|
|
||||||
{rating} Rating: {count} responses
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Bar dataKey="count">
|
|
||||||
{likelihoodCounts.map((_, index) => (
|
|
||||||
<Cell
|
|
||||||
key={`cell-${index}`}
|
|
||||||
fill={
|
|
||||||
index === 0 ? "#ef4444" : // red
|
|
||||||
index === 1 ? "#f97316" : // orange
|
|
||||||
index === 2 ? "#eab308" : // yellow
|
|
||||||
index === 3 ? "#84cc16" : // lime
|
|
||||||
"#10b981" // green
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Bar>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TypeformDashboard = () => {
|
const TypeformDashboard = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -487,18 +327,55 @@ const TypeformDashboard = () => {
|
|||||||
|
|
||||||
const metrics = loading ? null : calculateMetrics();
|
const metrics = loading ? null : calculateMetrics();
|
||||||
|
|
||||||
|
// Find the newest response across both forms
|
||||||
|
const getNewestResponse = () => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const newestResponse = getNewestResponse();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Alert variant="destructive">
|
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<CardContent className="p-4">
|
||||||
<AlertTitle>Error</AlertTitle>
|
<div className="p-4 m-6 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/10 rounded-lg border border-red-200 dark:border-red-900/20">
|
||||||
<AlertDescription>{error}</AlertDescription>
|
{error}
|
||||||
</Alert>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
})) : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
|
<CardHeader className="p-6 pb-0">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
Customer Surveys
|
||||||
|
</CardTitle>
|
||||||
|
{newestResponse && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Newest response: {format(new Date(newestResponse), "MMM d, h:mm a")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -508,41 +385,195 @@ const TypeformDashboard = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
||||||
{renderProductRelevanceBar(metrics)}
|
|
||||||
{renderLikelihoodChart(metrics, formData.form2.responses)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 xl:grid-cols-12 gap-6">
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<div className="col-span-4">
|
<CardHeader>
|
||||||
<Card>
|
<div className="flex items-baseline justify-between">
|
||||||
<CardHeader>
|
<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">Reasons for Not Ordering</CardTitle>
|
<span className={`text-2xl font-bold ${
|
||||||
|
metrics.winback.averageRating <= 1 ? "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}
|
||||||
|
<span className="text-base font-normal text-muted-foreground">/5 avg</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
<div className="h-[200px]">
|
||||||
<TableHeader>
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<TableRow>
|
<BarChart
|
||||||
<TableHead>Reason</TableHead>
|
data={likelihoodCounts}
|
||||||
<TableHead className="text-right">Count</TableHead>
|
margin={{ top: 0, right: 0, left: -20, bottom: 0 }}
|
||||||
<TableHead className="text-right w-[80px]">%</TableHead>
|
>
|
||||||
</TableRow>
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||||
</TableHeader>
|
<XAxis
|
||||||
<TableBody>
|
dataKey="rating"
|
||||||
{metrics.winback.reasons.map((reason, index) => (
|
tickFormatter={(value) => {
|
||||||
<TableRow key={index}>
|
return value === "1" ? "Not at all likely" : value === "5" ? "Extremely likely" : "";
|
||||||
<TableCell className="font-medium">{reason.reason}</TableCell>
|
}}
|
||||||
<TableCell className="text-right">{reason.count}</TableCell>
|
textAnchor="middle"
|
||||||
<TableCell className="text-right">{reason.percentage}%</TableCell>
|
interval={0}
|
||||||
</TableRow>
|
height={50}
|
||||||
))}
|
className="text-muted-foreground"
|
||||||
</TableBody>
|
/>
|
||||||
</Table>
|
<YAxis className="text-muted-foreground" />
|
||||||
|
<Tooltip
|
||||||
|
content={({ payload }) => {
|
||||||
|
if (payload && payload.length) {
|
||||||
|
const { rating, count } = payload[0].payload;
|
||||||
|
return (
|
||||||
|
<Card className="p-3 shadow-lg bg-white dark:bg-gray-900/60 backdrop-blur-sm border-none">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{rating} Rating: {count} responses
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="count">
|
||||||
|
{likelihoodCounts.map((_, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={
|
||||||
|
index === 0 ? "#ef4444" : // red
|
||||||
|
index === 1 ? "#f97316" : // orange
|
||||||
|
index === 2 ? "#eab308" : // yellow
|
||||||
|
index === 3 ? "#84cc16" : // lime
|
||||||
|
"#10b981" // green
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-baseline justify-between">
|
||||||
|
<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">
|
||||||
|
<span className="text-2xl font-bold text-green-600 dark:text-green-500">
|
||||||
|
{metrics.productRelevance.yesPercentage}% Positive
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="h-[100px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={[{
|
||||||
|
yes: metrics.productRelevance.yesCount,
|
||||||
|
no: metrics.productRelevance.noCount,
|
||||||
|
total: metrics.productRelevance.yesCount + metrics.productRelevance.noCount
|
||||||
|
}]}
|
||||||
|
layout="vertical"
|
||||||
|
stackOffset="expand"
|
||||||
|
margin={{ top: 0, right: 0, left: -20, bottom: 0 }}
|
||||||
|
>
|
||||||
|
<XAxis type="number" hide domain={[0, 1]} />
|
||||||
|
<YAxis type="category" hide />
|
||||||
|
<Tooltip
|
||||||
|
cursor={false}
|
||||||
|
content={({ payload }) => {
|
||||||
|
if (payload && payload.length) {
|
||||||
|
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);
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex justify-between items-center text-sm">
|
||||||
|
<span className="text-emerald-500 font-medium">Yes:</span>
|
||||||
|
<span className="ml-4 text-muted-foreground">{yesCount} ({yesPercent}%)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center text-sm">
|
||||||
|
<span className="text-red-500 font-medium">No:</span>
|
||||||
|
<span className="ml-4 text-muted-foreground">{noCount} ({noPercent}%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="yes" stackId="stack" fill="#10b981" radius={[4, 4, 0, 0]}>
|
||||||
|
<text
|
||||||
|
x="50%"
|
||||||
|
y="50%"
|
||||||
|
textAnchor="middle"
|
||||||
|
fill="#fff"
|
||||||
|
fontSize={14}
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{metrics.productRelevance.yesPercentage}%
|
||||||
|
</text>
|
||||||
|
</Bar>
|
||||||
|
<Bar dataKey="no" stackId="stack" fill="#ef4444" radius={[0, 0, 4, 4]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mt-2 text-md font-semibold mx-1 text-muted-foreground">
|
||||||
|
<div>Yes: {metrics.productRelevance.yesCount}</div>
|
||||||
|
<div>No: {metrics.productRelevance.noCount}</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4 lg:col-span-1 xl:col-span-4"><ProductRelevanceFeed responses={formData.form1.responses} /></div>
|
|
||||||
<div className="col-span-4 lg:col-span-1 xl:col-span-4"><WinbackFeed responses={formData.form2.responses} /></div>
|
<div className="grid grid-cols-2 xl:grid-cols-12 gap-6">
|
||||||
|
<div className="col-span-4">
|
||||||
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Reasons for Not Ordering</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<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">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="font-medium text-gray-900 dark:text-gray-100">Reason</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>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{metrics.winback.reasons.map((reason, index) => (
|
||||||
|
<TableRow key={index} className="hover:bg-muted/50 transition-colors">
|
||||||
|
<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>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-4 lg:col-span-1 xl:col-span-4">
|
||||||
|
<WinbackFeed responses={formData.form2.responses} />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-4 lg:col-span-1 xl:col-span-4">
|
||||||
|
<ProductRelevanceFeed responses={formData.form1.responses} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user