Condense and restyle frontend component

This commit is contained in:
2024-12-30 00:47:35 -05:00
parent e21d2d88d9
commit 7db9626c17

View File

@@ -7,7 +7,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { import {
Table, Table,
TableBody, TableBody,
@@ -20,17 +19,8 @@ import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { AlertCircle, Activity, Eye, MessageSquare, ThumbsUp, Clock, Users, Star, ArrowUp, ArrowDown, BarChart3 } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { format } from "date-fns"; import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { import {
BarChart, BarChart,
Bar, Bar,
@@ -40,9 +30,6 @@ import {
Tooltip, Tooltip,
ResponsiveContainer, ResponsiveContainer,
Cell, Cell,
PieChart,
Pie,
Legend,
ReferenceLine ReferenceLine
} from "recharts"; } from "recharts";
@@ -57,12 +44,6 @@ const FORM_NAMES = {
[FORM_IDS.FORM_2]: 'Winback Survey', [FORM_IDS.FORM_2]: 'Winback Survey',
}; };
const METRIC_COLORS = {
total: "#8b5cf6",
completed: "#10b981",
today: "#f59e0b",
};
// 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-card rounded-lg p-6">
@@ -102,9 +83,6 @@ const SkeletonTable = () => (
<TableHead> <TableHead>
<Skeleton className="h-4 w-[80px] bg-muted" /> <Skeleton className="h-4 w-[80px] bg-muted" />
</TableHead> </TableHead>
<TableHead className="text-right">
<Skeleton className="h-4 w-[60px] bg-muted ml-auto" />
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -117,10 +95,7 @@ const SkeletonTable = () => (
<Skeleton className="h-4 w-[90px] bg-muted" /> <Skeleton className="h-4 w-[90px] bg-muted" />
</TableCell> </TableCell>
<TableCell> <TableCell>
<Skeleton className="h-6 w-[70px] bg-muted" /> <Skeleton className="h-4 w-[70px] bg-muted" />
</TableCell>
<TableCell className="text-right">
<Skeleton className="h-4 w-[40px] bg-muted ml-auto" />
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
@@ -129,241 +104,6 @@ const SkeletonTable = () => (
</div> </div>
); );
const SkeletonMetricCard = () => (
<Card className="h-full">
<CardContent className="pt-6 h-full">
<div className="flex justify-between items-start">
<div className="flex-1 min-w-0">
<Skeleton className="h-4 w-24 mb-4 bg-muted" />
<div className="flex items-baseline gap-2">
<Skeleton className="h-8 w-20 bg-muted" />
<Skeleton className="h-4 w-12 bg-muted" />
</div>
</div>
<Skeleton className="h-5 w-5 rounded-full bg-muted" />
</div>
</CardContent>
</Card>
);
const CustomTooltip = ({ active, payload }) => {
if (active && payload && payload.length) {
return (
<Card className="p-3 shadow-lg bg-white dark:bg-gray-800 border-none">
<CardContent className="p-0 space-y-1">
{payload.map((entry, index) => (
<div key={index} className="flex justify-between items-center text-sm">
<span style={{ color: entry.fill }}>{entry.name}:</span>
<span className="font-medium ml-4">{entry.value.toLocaleString()}</span>
</div>
))}
</CardContent>
</Card>
);
}
return null;
};
const ResponseDialog = ({ response }) => (
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<Eye className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden">
<DialogHeader>
<DialogTitle>Response Details</DialogTitle>
<DialogDescription>
From {response.hidden?.name || 'Anonymous'} ({response.hidden?.email || 'No email'})
<br />
Submitted at {format(new Date(response.submitted_at), "PPpp")}
</DialogDescription>
</DialogHeader>
<ScrollArea className="mt-4 h-full max-h-[60vh] rounded-md border p-4">
<div className="space-y-4">
{response.answers?.map((answer, index) => (
<div key={index} className="space-y-2">
<h4 className="font-medium">{answer.field.type.replace(/_/g, ' ').toUpperCase()}</h4>
{answer.type === 'boolean' && (
<p>{answer.boolean ? 'Yes' : 'No'}</p>
)}
{answer.type === 'number' && (
<p>Rating: {answer.number}/5</p>
)}
{answer.type === 'choices' && answer.choices?.labels && (
<p>{answer.choices.labels.join(', ')}</p>
)}
{answer.type === 'text' && (
<p className="text-sm text-muted-foreground">{answer.text}</p>
)}
</div>
))}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
);
const getResponseSummary = (response) => {
if (!response.answers?.length) return 'No answers';
const firstAnswer = response.answers[0];
switch (firstAnswer.type) {
case 'boolean':
return firstAnswer.boolean ? 'Yes' : 'No';
case 'number':
return `Rating: ${firstAnswer.number}/5`;
case 'choices':
return firstAnswer.choices?.labels?.[0] || 'Multiple choice answer';
case 'text':
return firstAnswer.text ? (firstAnswer.text.slice(0, 50) + (firstAnswer.text.length > 50 ? '...' : '')) : 'Text answer';
default:
return `${firstAnswer.type} answer`;
}
};
const renderResponsesTable = (responses) => {
if (!responses?.items?.length) {
return (
<div className="flex items-center justify-center h-[400px] text-muted-foreground">
<div className="text-center">
<Activity className="h-12 w-12 mx-auto mb-4" />
<div className="font-medium mb-2">No responses yet</div>
<div className="text-sm text-muted-foreground">
Responses will appear here when they come in
</div>
</div>
</div>
);
}
return (
<Card className="mt-6">
<CardHeader>
<CardTitle className="text-lg font-semibold">Form Responses</CardTitle>
<CardDescription>
Latest form submissions and their status
</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[500px]">
<Table>
<TableHeader>
<TableRow>
<TableHead>Submitted At</TableHead>
<TableHead>Respondent</TableHead>
<TableHead>Response Summary</TableHead>
<TableHead>Platform</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{responses.items.map((response) => (
<TableRow key={response.token}>
<TableCell>
{format(new Date(response.submitted_at), "MMM d, yyyy HH:mm")}
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="font-medium">{response.hidden?.name || 'Anonymous'}</span>
<span className="text-xs text-muted-foreground">{response.hidden?.email || 'No email'}</span>
</div>
</TableCell>
<TableCell>
<span className="truncate block max-w-[200px]" title={getResponseSummary(response)}>
{getResponseSummary(response)}
</span>
</TableCell>
<TableCell>
<Badge variant="outline">
{response.metadata.platform}
</Badge>
</TableCell>
<TableCell className="text-right">
<ResponseDialog response={response} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</ScrollArea>
</CardContent>
</Card>
);
};
const calculateProductRelevanceInsights = (responses) => {
if (!responses?.items?.length) return null;
const yesResponses = responses.items.filter(r =>
r.answers?.some(a => a.type === 'boolean' && a.boolean === true)
).length;
const noResponses = responses.items.filter(r =>
r.answers?.some(a => a.type === 'boolean' && a.boolean === false)
).length;
const total = yesResponses + noResponses;
const yesPercentage = Math.round((yesResponses / total) * 100) || 0;
return {
metrics: [
{ name: 'Yes', value: yesResponses, color: "#10b981" },
{ name: 'No', value: noResponses, color: "#ef4444" },
],
summary: {
yes_percentage: yesPercentage,
total_responses: total
}
};
};
const calculateWinbackInsights = (responses) => {
if (!responses?.items?.length) return null;
// Get likelihood ratings
const likelihoodAnswers = responses.items
.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
: 0;
// Get reasons for not ordering
const reasonsMap = new Map();
responses.items.forEach(response => {
const reasonsAnswer = response.answers?.find(a => a.type === 'choices');
if (reasonsAnswer?.choices?.labels) {
reasonsAnswer.choices.labels.forEach(label => {
reasonsMap.set(label, (reasonsMap.get(label) || 0) + 1);
});
}
});
const sortedReasons = Array.from(reasonsMap.entries())
.sort(([, a], [, b]) => b - a)
.map(([label, count]) => ({
name: label,
value: count,
color: "#8b5cf6"
}));
return {
likelihood: {
ratings: [...Array(5)].map((_, i) => ({
name: (i + 1).toString(),
value: likelihoodAnswers.filter(r => r === i + 1).length,
color: "#8b5cf6"
})),
average: averageLikelihood
},
reasons: sortedReasons
};
};
const ResponseFeed = ({ responses, title, renderSummary }) => ( const ResponseFeed = ({ responses, title, renderSummary }) => (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
@@ -371,23 +111,31 @@ const ResponseFeed = ({ responses, title, renderSummary }) => (
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ScrollArea className="h-[400px]"> <ScrollArea className="h-[400px]">
<div className="space-y-2"> <div className="divide-y divide-gray-100 dark:divide-gray-800">
{responses.items.map((response) => ( {responses.items.map((response) => (
<Card key={response.token} className="p-3"> <div key={response.token} className="p-4 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
<div className="flex justify-between items-start gap-4"> <div className="space-y-1.5">
<div className="min-w-0 flex-1"> <div className="flex items-center justify-between">
<div className="font-medium truncate"> <div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{response.hidden?.name || 'Anonymous'} {response.hidden?.name || 'Anonymous'}
</span>
{response.hidden?.email && (
<span className="text-sm text-muted-foreground">
({response.hidden.email})
</span>
)}
</div> </div>
<div className="text-sm text-muted-foreground"> <time
{format(new Date(response.submitted_at), "MMM d, yyyy HH:mm")} className="text-xs text-muted-foreground"
dateTime={response.submitted_at}
>
{format(new Date(response.submitted_at), "MMM d, h:mm a")}
</time>
</div> </div>
<div className="mt-1">
{renderSummary(response)} {renderSummary(response)}
</div> </div>
</div> </div>
</div>
</Card>
))} ))}
</div> </div>
</ScrollArea> </ScrollArea>
@@ -401,14 +149,20 @@ const ProductRelevanceFeed = ({ 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;
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center gap-2">
<Badge variant={answer?.boolean ? "success" : "destructive"}> <Badge
className={
answer?.boolean ? "bg-green-200 text-green-700" : "bg-red-200 text-red-700"
}
>
{answer?.boolean ? "Yes" : "No"} {answer?.boolean ? "Yes" : "No"}
</Badge> </Badge>
{response.answers?.find(a => a.type === 'text')?.text && ( {textAnswer && (
<span className="text-sm"> <span className="text-sm text-foreground">
"{response.answers.find(a => a.type === 'text').text}" "{textAnswer}"
</span> </span>
)} )}
</div> </div>
@@ -424,25 +178,39 @@ const WinbackFeed = ({ responses }) => (
renderSummary={(response) => { renderSummary={(response) => {
const likelihoodAnswer = response.answers?.find(a => a.type === 'number'); const likelihoodAnswer = response.answers?.find(a => a.type === 'number');
const reasonsAnswer = response.answers?.find(a => a.type === 'choices'); const reasonsAnswer = response.answers?.find(a => a.type === 'choices');
const feedbackAnswer = response.answers?.find(a => a.type === 'text'); const otherAnswer = response.answers?.find(a => a.type === 'text' && a.field.ref.includes('other'));
const feedbackAnswer = response.answers?.find(a => a.type === 'text' && !a.field.ref.includes('other'));
return ( return (
<div className="space-y-2"> <div className="space-y-1.5">
<div className="flex items-center space-x-2"> <div className="flex items-center gap-2">
<Badge variant={likelihoodAnswer?.number >= 4 ? "success" : "warning"}> <Badge
Rating: {likelihoodAnswer?.number}/5 className={
likelihoodAnswer?.number === 1 ? "bg-red-200 text-red-700" :
likelihoodAnswer?.number === 2 ? "bg-orange-200 text-orange-700" :
likelihoodAnswer?.number === 3 ? "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
</Badge> </Badge>
{(reasonsAnswer?.choices?.labels || []).map((label, idx) => (
<Badge key={idx} variant="secondary" className="text-xs">
{label}
</Badge>
))}
</div> </div>
{reasonsAnswer?.choices?.labels && ( {otherAnswer?.text && (
<div className="text-sm"> <div className="text-sm">
<span className="font-medium">Reasons:</span>{" "} <span className="font-medium">Other:</span>{" "}
{reasonsAnswer.choices.labels.join(", ")} {otherAnswer.text}
</div> </div>
)} )}
{feedbackAnswer?.text && ( {feedbackAnswer?.text && (
<div className="text-sm"> <div className="text-sm">
<span className="font-medium">Feedback:</span>{" "} {feedbackAnswer.text}
"{feedbackAnswer.text}"
</div> </div>
)} )}
</div> </div>
@@ -454,10 +222,14 @@ const WinbackFeed = ({ responses }) => (
const renderProductRelevanceBar = (metrics) => ( const renderProductRelevanceBar = (metrics) => (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg font-semibold">Product Relevance Results</CardTitle> <div className="flex items-baseline justify-between">
<CardDescription> <CardTitle className="text-lg font-semibold">Were the suggested products in this email relevant to you?</CardTitle>
{metrics.productRelevance.yesPercentage}% Positive {metrics.productRelevance.yesCount} Yes / {metrics.productRelevance.noCount} No <div className="flex flex-col items-end">
</CardDescription> <span className="text-2xl font-bold text-green-600">
{metrics.productRelevance.yesPercentage}% Positive
</span>
</div>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="h-[100px]"> <div className="h-[100px]">
@@ -470,11 +242,12 @@ const renderProductRelevanceBar = (metrics) => (
}]} }]}
layout="vertical" layout="vertical"
stackOffset="expand" stackOffset="expand"
margin={{ top: 10, right: 0, left: -20, bottom: 0 }} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}
> >
<XAxis type="number" hide domain={[0, 1]} /> <XAxis type="number" hide domain={[0, 1]} />
<YAxis type="category" hide /> <YAxis type="category" hide />
<Tooltip <Tooltip
cursor={false}
content={({ payload }) => { content={({ payload }) => {
if (payload && payload.length) { if (payload && payload.length) {
const yesCount = payload[0].payload.yes; const yesCount = payload[0].payload.yes;
@@ -483,38 +256,50 @@ const renderProductRelevanceBar = (metrics) => (
const yesPercent = Math.round((yesCount / total) * 100); const yesPercent = Math.round((yesCount / total) * 100);
const noPercent = Math.round((noCount / total) * 100); const noPercent = Math.round((noCount / total) * 100);
return ( return (
<div className="bg-white dark:bg-gray-800 p-2 rounded-md shadow-md border"> <Card className="p-3 shadow-lg bg-white dark:bg-gray-800 border-none">
<div className="text-sm"> <CardContent className="p-0 space-y-2">
<div className="text-emerald-500">Yes: {yesCount} ({yesPercent}%)</div> <div className="space-y-1">
<div className="text-red-500">No: {noCount} ({noPercent}%)</div> <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>
</div> </div>
</CardContent>
</Card>
); );
} }
return null; return null;
}} }}
/> />
<Bar dataKey="yes" stackId="stack" fill="#10b981" /> <Bar dataKey="yes" stackId="stack" fill="#10b981" radius={[4, 4, 0, 0]}>
<Bar dataKey="no" stackId="stack" fill="#ef4444" /> <text
<ReferenceLine x="50%"
x={metrics.productRelevance.yesPercentage / 100} y="50%"
stroke="#6b7280" textAnchor="middle"
strokeDasharray="3 3" fill="#fff"
label={{ fontSize={14}
value: `${metrics.productRelevance.yesPercentage}%`, fontWeight="bold"
position: 'right', >
fill: '#6b7280' {metrics.productRelevance.yesPercentage}%
}} </text>
/> </Bar>
<Bar dataKey="no" stackId="stack" fill="#ef4444" radius={[0, 0, 4, 4]} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </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>
); );
const renderLikelihoodChart = (metrics, responses) => { const renderLikelihoodChart = (metrics, responses) => {
// Get likelihood distribution from responses
const likelihoodCounts = [1, 2, 3, 4, 5].map(rating => ({ const likelihoodCounts = [1, 2, 3, 4, 5].map(rating => ({
rating: rating.toString(), rating: rating.toString(),
count: responses.items count: responses.items
@@ -525,34 +310,46 @@ const renderLikelihoodChart = (metrics, responses) => {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg font-semibold">Return Likelihood</CardTitle> <div className="flex items-baseline justify-between">
<CardDescription> <CardTitle className="text-lg font-semibold">How likely are you to place another order with us?</CardTitle>
Average Rating: {metrics.winback.averageRating}/5 {responses.items.length} Total Responses <span className={`text-2xl font-bold ${
</CardDescription> 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> </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: 20, right: 20, left: -10, bottom: 20 }} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}
> >
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" /> <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis <XAxis
dataKey="rating" dataKey="rating"
label={{ value: 'Rating', position: 'bottom', offset: -10 }} tickFormatter={(value) => {
/> return value === "1" ? "Not at all likely" : value === "5" ? "Extremely likely" : "";
<YAxis }}
label={{ value: 'Responses', angle: -90, position: 'insideLeft', offset: 10 }} textAnchor="middle"
interval={0}
height={50}
/> />
<YAxis />
<Tooltip <Tooltip
content={({ payload }) => { content={({ payload }) => {
if (payload && payload.length) { if (payload && payload.length) {
const { rating, count } = payload[0].payload; const { rating, count } = payload[0].payload;
return ( return (
<div className="bg-white dark:bg-gray-800 p-2 rounded-md shadow-md border"> <div className="bg-white dark:bg-gray-800 p-2 rounded-md shadow-md border">
<div className="text-sm"> <div className="text-sm font-medium">
Rating {rating}: {count} responses {rating} Rating: {count} responses
</div> </div>
</div> </div>
); );
@@ -564,20 +361,16 @@ const renderLikelihoodChart = (metrics, responses) => {
{likelihoodCounts.map((_, index) => ( {likelihoodCounts.map((_, index) => (
<Cell <Cell
key={`cell-${index}`} key={`cell-${index}`}
fill={index >= 3 ? "#10b981" : index === 2 ? "#f59e0b" : "#ef4444"} fill={
index === 0 ? "#ef4444" : // red
index === 1 ? "#f97316" : // orange
index === 2 ? "#eab308" : // yellow
index === 3 ? "#84cc16" : // lime
"#10b981" // green
}
/> />
))} ))}
</Bar> </Bar>
<ReferenceLine
x={metrics.winback.averageRating.toString()}
stroke="#6b7280"
strokeDasharray="3 3"
label={{
value: `Avg: ${metrics.winback.averageRating}`,
position: 'top',
fill: '#6b7280'
}}
/>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@@ -586,88 +379,6 @@ const renderLikelihoodChart = (metrics, responses) => {
); );
}; };
const MetricCard = ({
title,
value,
delta,
suffix = "",
icon: Icon,
colorClass = "blue",
more_is_better = true,
loading = false,
}) => {
const getDeltaColor = (d) => {
if (d === 0) return "text-gray-600 dark:text-gray-400";
const isPositive = d > 0;
return isPositive === more_is_better
? "text-green-600 dark:text-green-500"
: "text-red-600 dark:text-red-500";
};
const formatDelta = (d) => {
if (d === undefined || d === null) return null;
if (d === 0) return "0";
return Math.abs(d) + suffix;
};
return (
<Card className="h-full">
<CardContent className="pt-6 h-full">
<div className="flex justify-between items-start">
<div className="flex-1 min-w-0">
{loading ? (
<>
<Skeleton className="h-4 w-24 mb-4 bg-muted" />
<div className="flex items-baseline gap-2">
<Skeleton className="h-8 w-20 bg-muted" />
<Skeleton className="h-4 w-12 bg-muted" />
</div>
</>
) : (
<>
<p className="text-sm font-medium text-muted-foreground">{title}</p>
<div className="flex items-baseline gap-2">
<p className="text-2xl font-bold">
{typeof value === "number"
? value.toLocaleString() + suffix
: value}
</p>
{delta !== undefined && delta !== 0 && (
<div className={`flex items-center ${getDeltaColor(delta)}`}>
{delta > 0 ? (
<ArrowUp className="w-3 h-3" />
) : (
<ArrowDown className="w-3 h-3" />
)}
<span className="text-xs font-medium">
{formatDelta(delta)}
</span>
</div>
)}
</div>
</>
)}
</div>
{!loading && Icon && (
<Icon className={`h-5 w-5 flex-shrink-0 ml-2 ${
colorClass === "blue" ? "text-blue-500" :
colorClass === "green" ? "text-green-500" :
colorClass === "purple" ? "text-purple-500" :
colorClass === "indigo" ? "text-indigo-500" :
colorClass === "orange" ? "text-orange-500" :
"text-blue-500"
}`} />
)}
{loading && (
<Skeleton className="h-5 w-5 rounded-full bg-muted" />
)}
</div>
</CardContent>
</Card>
);
};
// Update the TypeformDashboard component to use pagination
const TypeformDashboard = () => { const TypeformDashboard = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@@ -677,7 +388,7 @@ const TypeformDashboard = () => {
}); });
const fetchResponses = async (formId, before = null) => { const fetchResponses = async (formId, before = null) => {
const params = { page_size: 1000 }; // Start with max page size 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 });
@@ -787,7 +498,8 @@ const TypeformDashboard = () => {
} }
return ( return (
<div className="space-y-6"> <Card>
<CardContent className="space-y-6">
{loading ? ( {loading ? (
<div className="space-y-6"> <div className="space-y-6">
<SkeletonChart /> <SkeletonChart />
@@ -795,11 +507,13 @@ const TypeformDashboard = () => {
</div> </div>
) : ( ) : (
<> <>
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
{renderProductRelevanceBar(metrics)} {renderProductRelevanceBar(metrics)}
{renderLikelihoodChart(metrics, formData.form2.responses)} {renderLikelihoodChart(metrics, formData.form2.responses)}
</div> </div>
<div className="grid grid-cols-2 xl:grid-cols-12 gap-6">
<div className="col-span-4">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg font-semibold">Reasons for Not Ordering</CardTitle> <CardTitle className="text-lg font-semibold">Reasons for Not Ordering</CardTitle>
@@ -825,14 +539,15 @@ const TypeformDashboard = () => {
</Table> </Table>
</CardContent> </CardContent>
</Card> </Card>
</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 gap-6">
<ProductRelevanceFeed responses={formData.form1.responses} />
<WinbackFeed responses={formData.form2.responses} />
</div> </div>
</> </>
)} )}
</div> </CardContent>
</Card>
); );
}; };