Files
dataroom/components/documents/video-analytics.tsx
Marc Seitz 8083a09075 feat: small fixes
1. visits to views
2. name on dataroom from template
3. trial days
4. reactions to %. of views
2025-10-25 17:51:06 +02:00

232 lines
6.7 KiB
TypeScript

import { DocumentVersion } from "@prisma/client";
import { formatDistanceToNow } from "date-fns";
import {
Area,
AreaChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import useSWR from "swr";
import { fetcher } from "@/lib/utils";
import StatsElement from "@/components/documents/stats-element";
import VideoChartPlaceholder from "@/components/documents/video-chart-placeholder";
import { Card, CardContent } from "@/components/ui/card";
import LoadingSpinner from "@/components/ui/loading-spinner";
interface VideoAnalyticsProps {
teamId: string;
documentId: string;
primaryVersion: DocumentVersion;
}
export default function VideoAnalytics({
teamId,
documentId,
primaryVersion,
}: VideoAnalyticsProps) {
const { data, error, isLoading } = useSWR<{
overall: {
unique_views: number;
total_views: number;
total_watch_time: number;
avg_view_duration: number;
last_viewed_at: string;
first_viewed_at: string;
view_distribution: Array<{
start_time: number;
unique_views: number;
total_views: number;
}>;
} | null;
}>(`/api/teams/${teamId}/documents/${documentId}/video-analytics`, fetcher);
if (error) {
console.error("Error loading video analytics:", error);
return null;
}
if (isLoading) {
return (
<Card>
<CardContent className="flex h-[300px] items-center justify-center">
<LoadingSpinner />
</CardContent>
</Card>
);
}
if (!data?.overall) {
const emptyStats = [
{
name: "Total views",
value: "0",
active: false,
},
{
name: "Watch time",
value: "0:00",
unit: "minutes",
active: false,
},
{
name: "Average view duration",
value: "0:00",
unit: "minutes",
active: false,
},
];
return (
<div className="space-y-4">
<VideoChartPlaceholder length={primaryVersion.length} />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{emptyStats.map((stat, index) => (
<StatsElement key={stat.name} stat={stat} statIdx={index} />
))}
</div>
</div>
);
}
const formatTime = (seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
};
const formatTooltipTime = (value: number) => {
return formatTime(value);
};
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const uniqueViews = payload[1].value;
const playbackCount = payload[0].value;
const intensity = playbackCount / uniqueViews || 1;
return (
<div className="space-y-1 rounded-md border bg-background p-2 text-sm">
<p className="font-medium">{formatTooltipTime(label)}</p>
<div className="space-y-0.5 text-muted-foreground">
<p className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-emerald-500" />
{uniqueViews} unique viewer{uniqueViews !== 1 ? "s" : ""}
</p>
<p className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-[#3B82F6]" />
{playbackCount} playback{playbackCount !== 1 ? "s" : ""}
</p>
<p className="pt-1 text-xs">
Replayed {(intensity - 1).toFixed(1)}x on average
</p>
</div>
</div>
);
}
return null;
};
const stats = [
{
name: "Total views",
value: data.overall.unique_views.toString(),
active: true,
},
{
name: "View time",
value: formatTime(data.overall.total_watch_time),
unit: "minutes",
active: true,
},
{
name: "Average view duration",
value: formatTime(data.overall.avg_view_duration),
unit: "minutes",
active: true,
},
];
return (
<div className="space-y-4">
<div className="mt-2 flex items-center justify-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<div className="h-3 w-3 rounded-full bg-emerald-500" />
<span>Unique Viewers</span>
</div>
<div className="flex items-center gap-2">
<div className="h-3 w-3 rounded-full bg-[#3B82F6]" />
<span>Playback Count</span>
</div>
</div>
<div className="-mx-9 h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data.overall.view_distribution}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
>
<XAxis
dataKey="start_time"
tickFormatter={formatTooltipTime}
stroke="#888888"
fontSize={12}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickFormatter={(value) => Math.floor(value).toString()}
domain={[0, (dataMax: number) => dataMax + 1]}
padding={{ top: 20 }}
allowDecimals={false}
/>
<Tooltip content={<CustomTooltip />} />
<defs>
<linearGradient
id="uniqueViewsGradient"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
<linearGradient id="playbackGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3B82F6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3B82F6" stopOpacity={0} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="total_views"
stroke="#3B82F6"
strokeWidth={2}
fill="url(#playbackGradient)"
dot={false}
name="Playback Count"
/>
<Area
type="monotone"
dataKey="unique_views"
stroke="#10b981"
strokeWidth={2}
fill="url(#uniqueViewsGradient)"
dot={false}
name="Unique Viewers"
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{stats.map((stat, index) => (
<StatsElement key={stat.name} stat={stat} statIdx={index} />
))}
</div>
</div>
);
}