Files
shorturl-analytics/app/links/[id]/page.tsx
2025-03-21 12:08:37 +08:00

1355 lines
60 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import {
LinkOverviewData,
ConversionFunnelData,
VisitTrendsData,
LinkPerformanceData,
PlatformDistributionData,
DeviceAnalysisData,
PopularReferrersData,
QrCodeAnalysisData,
DeviceItem,
ReferrerItem,
} from "@/app/api/types";
import { TimeGranularity } from "@/lib/analytics";
interface LinkDetails {
id: string;
name: string;
shortUrl: string;
originalUrl: string;
creator: string;
createdAt: string;
visits: number;
visitChange: number;
uniqueVisitors: number;
uniqueVisitorsChange: number;
avgTime: string;
avgTimeChange: number;
conversionRate: number;
conversionChange: number;
status: "active" | "inactive" | "expired";
tags: string[];
}
export default function LinkDetailsPage({
params,
}: {
params: { id: string };
}) {
const router = useRouter();
const [linkId, setLinkId] = useState<string>("");
const [linkDetails, setLinkDetails] = useState<LinkDetails | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<
| "overview"
| "referrers"
| "devices"
| "locations"
| "performance"
| "qrCodes"
>("overview");
// 添加state变量存储分析数据
const [overviewData, setOverviewData] = useState<LinkOverviewData | null>(
null
);
const [funnelData, setFunnelData] = useState<ConversionFunnelData | null>(
null
);
const [trendsData, setTrendsData] = useState<VisitTrendsData | null>(null);
// 添加新的state变量存储新API的数据
const [performanceData, setPerformanceData] =
useState<LinkPerformanceData | null>(null);
const [platformData, setPlatformData] =
useState<PlatformDistributionData | null>(null);
const [deviceData, setDeviceData] = useState<DeviceAnalysisData | null>(null);
const [referrersData, setReferrersData] =
useState<PopularReferrersData | null>(null);
const [qrCodeData, setQrCodeData] = useState<QrCodeAnalysisData | null>(null);
const [timeGranularity, setTimeGranularity] = useState<TimeGranularity>(
TimeGranularity.DAY
);
const [dateRange, setDateRange] = useState({
startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0], // 30天前
endDate: new Date().toISOString().split("T")[0], // 今天
});
// 获取并设置linkId
useEffect(() => {
const loadParams = async () => {
const resolvedParams = await params;
setLinkId(resolvedParams.id);
};
loadParams();
}, [params]);
// 获取链接详情
useEffect(() => {
if (!linkId) return; // 等待linkId加载完成
const fetchLinkDetails = async () => {
setLoading(true);
try {
// 调用API获取链接基本信息
const response = await fetch(`/api/links/${linkId}/details`);
if (!response.ok) {
throw new Error(
`Failed to fetch link details: ${response.statusText}`
);
}
const data = await response.json();
// 转换API数据为UI所需格式
setLinkDetails({
id: data.link_id,
name: data.title || "Untitled Link",
shortUrl: generateShortUrlDisplay(data.link_id, data.original_url),
originalUrl: data.original_url,
creator: data.created_by,
createdAt: new Date(data.created_at).toLocaleDateString(),
visits: 0, // 不包含统计数据
visitChange: 0,
uniqueVisitors: 0,
uniqueVisitorsChange: 0,
avgTime: "0m 0s",
avgTimeChange: 0,
conversionRate: 0,
conversionChange: 0,
status: data.is_active ? "active" : "inactive",
tags: data.tags || [],
});
// 加载分析数据
await fetchAnalyticsData(data.link_id);
} catch (error) {
console.error("Failed to fetch link details:", error);
setLoading(false);
}
};
fetchLinkDetails();
}, [linkId]);
// 获取链接分析数据
const fetchAnalyticsData = async (id: string) => {
try {
// 构建查询参数
const queryParams = new URLSearchParams({
linkId: id,
startDate: dateRange.startDate,
endDate: dateRange.endDate,
});
// 并行获取所有数据
const [
overviewResponse,
funnelResponse,
trendsResponse,
performanceResponse,
platformResponse,
deviceResponse,
referrersResponse,
qrCodeResponse,
] = await Promise.all([
fetch(`/api/analytics/overview?${queryParams}`),
fetch(`/api/analytics/funnel?${queryParams}`),
fetch(
`/api/analytics/trends?${queryParams}&granularity=${timeGranularity}`
),
fetch(`/api/analytics/link-performance?${queryParams}`),
fetch(`/api/analytics/platform-distribution?${queryParams}`),
fetch(`/api/analytics/device-analysis?${queryParams}`),
fetch(`/api/analytics/popular-referrers?${queryParams}`),
fetch(`/api/analytics/qr-code-analysis?${queryParams}`),
]);
// 检查所有响应
if (
!overviewResponse.ok ||
!funnelResponse.ok ||
!trendsResponse.ok ||
!performanceResponse.ok ||
!platformResponse.ok ||
!deviceResponse.ok ||
!referrersResponse.ok ||
!qrCodeResponse.ok
) {
throw new Error("Failed to fetch analytics data");
}
// 解析所有响应数据
const [
overviewResult,
funnelResult,
trendsResult,
performanceResult,
platformResult,
deviceResult,
referrersResult,
qrCodeResult,
] = await Promise.all([
overviewResponse.json(),
funnelResponse.json(),
trendsResponse.json(),
performanceResponse.json(),
platformResponse.json(),
deviceResponse.json(),
referrersResponse.json(),
qrCodeResponse.json(),
]);
// 设置状态
setOverviewData(overviewResult);
setFunnelData(funnelResult);
setTrendsData(trendsResult);
setPerformanceData(performanceResult);
setPlatformData(platformResult);
setDeviceData(deviceResult);
setReferrersData(referrersResult);
setQrCodeData(qrCodeResult);
// 更新链接详情中的统计数据
setLinkDetails((prev) => {
if (!prev) return prev;
return {
...prev,
visits: overviewResult.totalVisits,
uniqueVisitors: overviewResult.uniqueVisitors,
avgTime: formatTime(overviewResult.averageTimeSpent),
conversionRate: funnelResult.conversionRate,
};
});
setLoading(false);
} catch (error) {
console.error("Failed to fetch analytics data:", error);
setLoading(false);
}
};
// 格式化时间(秒转为分钟和秒)
const formatTime = (seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 60);
return `${minutes}m ${remainingSeconds}s`;
};
// 更新时间粒度并重新获取趋势数据
const updateTimeGranularity = (granularity: TimeGranularity) => {
setTimeGranularity(granularity);
if (linkId) {
const queryParams = new URLSearchParams({
linkId,
startDate: dateRange.startDate,
endDate: dateRange.endDate,
granularity,
});
fetch(`/api/analytics/trends?${queryParams}`)
.then((res) => res.json())
.then((data) => setTrendsData(data))
.catch((err) => console.error("Failed to update trends data:", err));
}
};
// 更新日期范围并重新获取所有数据
const updateDateRange = (startDate: string, endDate: string) => {
setDateRange({ startDate, endDate });
if (linkId) {
fetchAnalyticsData(linkId);
}
};
// 从link_id和原始URL生成短URL显示
const generateShortUrlDisplay = (
linkId: string,
originalUrl: string
): string => {
try {
// 尝试从原始URL提取域名
const urlObj = new URL(originalUrl);
const domain = urlObj.hostname.replace("www.", "");
// 使用link_id的前8个字符作为短代码
const shortCode = linkId.substring(0, 8);
return `${domain}/${shortCode}`;
} catch {
// 如果URL解析失败返回一个基于linkId的默认值
return `short.link/${linkId.substring(0, 8)}`;
}
};
const goBack = () => {
router.back();
};
return (
<div className="container px-4 py-8 mx-auto">
<div className="flex flex-col gap-8">
{/* 顶部导航栏 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<button
onClick={goBack}
className="flex items-center text-text-secondary hover:text-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-5 h-5 mr-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
<span>Back to Links</span>
</button>
</div>
<div className="flex space-x-2">
<button className="px-4 py-2 text-white rounded-lg bg-accent-blue hover:bg-blue-600">
Edit Link
</button>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center p-8">
<div className="w-12 h-12 border-4 rounded-full border-accent-blue border-t-transparent animate-spin"></div>
<p className="ml-4 text-text-secondary">Loading link details...</p>
</div>
) : linkDetails ? (
<>
{/* 链接基本信息卡片 */}
<div className="overflow-hidden border rounded-lg shadow-sm bg-card-bg border-card-border">
<div className="flex flex-col gap-6 p-6 md:flex-row">
<div className="flex-1">
<h1 className="mb-2 text-2xl font-bold text-foreground">
{linkDetails.name}
</h1>
<div className="space-y-4">
<div>
<span className="text-xs font-medium uppercase text-text-secondary">
Short URL
</span>
<div className="flex items-center mt-1">
<a
href={`https://${linkDetails.shortUrl}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium break-all text-accent-blue hover:underline"
>
{linkDetails.shortUrl}
</a>
<button
type="button"
className="ml-2 text-text-secondary hover:text-foreground"
onClick={() =>
navigator.clipboard.writeText(
`https://${linkDetails.shortUrl}`
)
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
/>
</svg>
</button>
</div>
</div>
<div>
<span className="text-xs font-medium uppercase text-text-secondary">
Original URL
</span>
<div className="mt-1">
<a
href={linkDetails.originalUrl}
target="_blank"
rel="noopener noreferrer"
className="break-all text-foreground hover:underline"
>
{linkDetails.originalUrl}
</a>
</div>
</div>
</div>
</div>
<div className="md:w-1/3">
<div className="space-y-4">
<div>
<span className="text-xs font-medium uppercase text-text-secondary">
Created By
</span>
<p className="mt-1 text-foreground">
{linkDetails.creator}
</p>
</div>
<div>
<span className="text-xs font-medium uppercase text-text-secondary">
Created At
</span>
<p className="mt-1 text-foreground">
{linkDetails.createdAt}
</p>
</div>
<div>
<span className="text-xs font-medium uppercase text-text-secondary">
Status
</span>
<div className="mt-1">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
${
linkDetails.status === "active"
? "bg-green-500/10 text-accent-green"
: linkDetails.status === "inactive"
? "bg-gray-500/10 text-text-secondary"
: "bg-red-500/10 text-accent-red"
}`}
>
{linkDetails.status.charAt(0).toUpperCase() +
linkDetails.status.slice(1)}
</span>
</div>
</div>
{linkDetails.tags && linkDetails.tags.length > 0 && (
<div>
<span className="text-xs font-medium uppercase text-text-secondary">
Tags
</span>
<div className="flex flex-wrap gap-2 mt-1">
{linkDetails.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-blue-500/10 rounded-full text-accent-blue"
>
{tag}
</span>
))}
</div>
</div>
)}
</div>
</div>
</div>
{/* 性能指标卡片 */}
<div className="p-6 border-t border-card-border">
<h4 className="mb-4 text-lg font-medium text-foreground">
Performance Metrics
</h4>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Total Visits */}
<div className="p-4 border rounded-lg bg-card-bg border-card-border">
<div className="flex items-center justify-between">
<h5 className="text-sm font-medium text-text-secondary">
Total Visits
</h5>
<span
className={`inline-flex items-center text-xs font-medium rounded-full px-2 py-0.5
${
linkDetails.visitChange >= 0
? "bg-green-500/10 text-accent-green"
: "bg-red-500/10 text-accent-red"
}`}
>
<svg
className={`w-3 h-3 mr-1 ${linkDetails.visitChange >= 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`}
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
{Math.abs(linkDetails.visitChange)}%
</span>
</div>
<div className="mt-2">
<p className="text-2xl font-bold text-foreground">
{linkDetails.visits.toLocaleString()}
</p>
</div>
</div>
{/* Unique Visitors */}
<div className="p-4 border rounded-lg bg-card-bg border-card-border">
<div className="flex items-center justify-between">
<h5 className="text-sm font-medium text-text-secondary">
Unique Visitors
</h5>
<span
className={`inline-flex items-center text-xs font-medium rounded-full px-2 py-0.5
${
linkDetails.uniqueVisitorsChange >= 0
? "bg-green-500/10 text-accent-green"
: "bg-red-500/10 text-accent-red"
}`}
>
<svg
className={`w-3 h-3 mr-1 ${linkDetails.uniqueVisitorsChange >= 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`}
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
{Math.abs(linkDetails.uniqueVisitorsChange)}%
</span>
</div>
<div className="mt-2">
<p className="text-2xl font-bold text-foreground">
{linkDetails.uniqueVisitors.toLocaleString()}
</p>
</div>
</div>
{/* Average Visit Time */}
<div className="p-4 border rounded-lg bg-card-bg border-card-border">
<div className="flex items-center justify-between">
<h5 className="text-sm font-medium text-text-secondary">
Avg. Time
</h5>
<span
className={`inline-flex items-center text-xs font-medium rounded-full px-2 py-0.5
${
linkDetails.avgTimeChange >= 0
? "bg-green-500/10 text-accent-green"
: "bg-red-500/10 text-accent-red"
}`}
>
<svg
className={`w-3 h-3 mr-1 ${linkDetails.avgTimeChange >= 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`}
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
{Math.abs(linkDetails.avgTimeChange)}%
</span>
</div>
<div className="mt-2">
<p className="text-2xl font-bold text-foreground">
{linkDetails.avgTime}
</p>
</div>
</div>
{/* Conversion Rate */}
<div className="p-4 border rounded-lg bg-card-bg border-card-border">
<div className="flex items-center justify-between">
<h5 className="text-sm font-medium text-text-secondary">
Conversion
</h5>
<span
className={`inline-flex items-center text-xs font-medium rounded-full px-2 py-0.5
${
linkDetails.conversionChange >= 0
? "bg-green-500/10 text-accent-green"
: "bg-red-500/10 text-accent-red"
}`}
>
<svg
className={`w-3 h-3 mr-1 ${linkDetails.conversionChange >= 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`}
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
{Math.abs(linkDetails.conversionChange)}%
</span>
</div>
<div className="mt-2">
<p className="text-2xl font-bold text-foreground">
{linkDetails.conversionRate}%
</p>
</div>
</div>
</div>
</div>
</div>
{/* 图表和详细数据部分 */}
<div className="overflow-hidden border rounded-lg shadow-sm bg-card-bg border-card-border">
<div className="border-b border-card-border">
<nav className="flex -mb-px">
{[
"overview",
"referrers",
"devices",
"locations",
"performance",
"qrCodes",
].map((tab) => (
<button
key={tab}
className={`py-4 px-6 font-medium text-sm border-b-2 ${
activeTab === tab
? "border-accent-blue text-accent-blue"
: "border-transparent text-text-secondary hover:text-foreground hover:border-card-border"
}`}
onClick={() =>
setActiveTab(
tab as
| "overview"
| "referrers"
| "devices"
| "locations"
| "performance"
| "qrCodes"
)
}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
</nav>
</div>
<div className="p-6">
{activeTab === "overview" && (
<div className="space-y-8">
{/* 日期范围选择器 */}
<div className="flex items-center justify-between">
<div className="text-lg font-medium text-foreground">
Analytics Overview
</div>
<div className="flex space-x-2">
<div className="relative">
<input
type="date"
value={dateRange.startDate}
onChange={(e) =>
updateDateRange(e.target.value, dateRange.endDate)
}
className="px-3 py-2 text-sm border rounded-md bg-card-bg border-card-border"
/>
</div>
<span className="self-center text-text-secondary">
to
</span>
<div className="relative">
<input
type="date"
value={dateRange.endDate}
onChange={(e) =>
updateDateRange(
dateRange.startDate,
e.target.value
)
}
className="px-3 py-2 text-sm border rounded-md bg-card-bg border-card-border"
min={dateRange.startDate}
/>
</div>
</div>
</div>
{/* 设备类型分布 */}
{overviewData && (
<div className="p-6 border rounded-lg bg-card-bg border-card-border">
<h3 className="mb-4 font-medium text-md text-foreground">
Device Types
</h3>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
<div className="flex flex-col items-center">
<div className="mb-2 text-sm font-medium text-text-secondary">
Mobile
</div>
<div className="text-2xl font-bold text-foreground">
{overviewData.deviceTypes.mobile}
</div>
<div className="mt-1 text-xs text-text-secondary">
{overviewData.totalVisits
? Math.round(
(overviewData.deviceTypes.mobile /
overviewData.totalVisits) *
100
)
: 0}
%
</div>
</div>
<div className="flex flex-col items-center">
<div className="mb-2 text-sm font-medium text-text-secondary">
Desktop
</div>
<div className="text-2xl font-bold text-foreground">
{overviewData.deviceTypes.desktop}
</div>
<div className="mt-1 text-xs text-text-secondary">
{overviewData.totalVisits
? Math.round(
(overviewData.deviceTypes.desktop /
overviewData.totalVisits) *
100
)
: 0}
%
</div>
</div>
<div className="flex flex-col items-center">
<div className="mb-2 text-sm font-medium text-text-secondary">
Tablet
</div>
<div className="text-2xl font-bold text-foreground">
{overviewData.deviceTypes.tablet}
</div>
<div className="mt-1 text-xs text-text-secondary">
{overviewData.totalVisits
? Math.round(
(overviewData.deviceTypes.tablet /
overviewData.totalVisits) *
100
)
: 0}
%
</div>
</div>
<div className="flex flex-col items-center">
<div className="mb-2 text-sm font-medium text-text-secondary">
Other
</div>
<div className="text-2xl font-bold text-foreground">
{overviewData.deviceTypes.other}
</div>
<div className="mt-1 text-xs text-text-secondary">
{overviewData.totalVisits
? Math.round(
(overviewData.deviceTypes.other /
overviewData.totalVisits) *
100
)
: 0}
%
</div>
</div>
</div>
</div>
)}
{/* 转化漏斗 */}
{funnelData && (
<div className="bg-card-bg border border-card-border rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-md font-medium text-foreground">
Conversion Funnel
</h3>
<div className="text-sm text-text-secondary">
Overall Conversion Rate:{" "}
<span className="font-medium text-accent-blue">
{(funnelData.conversionRate || 0).toFixed(2)}%
</span>
</div>
</div>
<div className="relative pt-2">
{funnelData.steps.map((step) => (
<div key={step.name} className="mb-4">
<div className="flex justify-between items-center mb-1">
<span className="text-sm font-medium">
{step.name}
</span>
<span className="text-sm text-text-secondary">
{step.value} ({(step.percent || 0).toFixed(1)}
%)
</span>
</div>
<div className="w-full bg-card-border rounded-full h-2.5">
<div
className="bg-accent-blue h-2.5 rounded-full"
style={{ width: `${step.percent || 0}%` }}
></div>
</div>
</div>
))}
</div>
</div>
)}
{/* 访问趋势 */}
{trendsData && (
<div className="p-6 border rounded-lg bg-card-bg border-card-border">
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium text-md text-foreground">
Visit Trends
</h3>
<div className="flex space-x-2">
{Object.values(TimeGranularity).map(
(granularity) => (
<button
key={granularity}
onClick={() =>
updateTimeGranularity(granularity)
}
className={`px-3 py-1 text-xs rounded-md ${
timeGranularity === granularity
? "bg-accent-blue text-white"
: "bg-card-bg text-text-secondary border border-card-border hover:bg-gray-100"
}`}
>
{granularity.charAt(0).toUpperCase() +
granularity.slice(1)}
</button>
)
)}
</div>
</div>
<div className="flex flex-col space-y-4">
<div className="flex justify-between text-sm text-text-secondary">
<div>
Total Visits:{" "}
<span className="font-medium text-foreground">
{trendsData.totals.visits}
</span>
</div>
<div>
Unique Visitors:{" "}
<span className="font-medium text-foreground">
{trendsData.totals.uniqueVisitors}
</span>
</div>
</div>
{/* 简单趋势表格 */}
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-card-border">
<thead>
<tr>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Time
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Visits
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Unique Visitors
</th>
</tr>
</thead>
<tbody className="divide-y bg-card-bg divide-card-border">
{trendsData.trends.map((trend, i) => (
<tr key={i}>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{trend.timestamp}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{trend.visits}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{trend.uniqueVisitors}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
</div>
)}
{activeTab === "performance" && performanceData && (
<div className="space-y-6">
<div className="p-6 border rounded-lg bg-card-bg border-card-border">
<h3 className="mb-4 font-medium text-md text-foreground">
Link Performance
</h3>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Total Clicks
</div>
<div className="text-2xl font-bold text-foreground">
{performanceData.totalClicks}
</div>
</div>
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Unique Visitors
</div>
<div className="text-2xl font-bold text-foreground">
{performanceData.uniqueVisitors}
</div>
</div>
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Bounce Rate
</div>
<div className="text-2xl font-bold text-foreground">
{performanceData.bounceRate}%
</div>
</div>
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Conversion Rate
</div>
<div className="text-2xl font-bold text-foreground">
{performanceData.conversionRate}%
</div>
</div>
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Avg. Time Spent
</div>
<div className="text-2xl font-bold text-foreground">
{formatTime(performanceData.averageTimeSpent)}
</div>
</div>
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Active Days
</div>
<div className="text-2xl font-bold text-foreground">
{performanceData.activeDays}
</div>
</div>
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Unique Referrers
</div>
<div className="text-2xl font-bold text-foreground">
{performanceData.uniqueReferrers}
</div>
</div>
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Last Click
</div>
<div className="text-lg font-medium text-foreground">
{performanceData.lastClickTime
? new Date(
performanceData.lastClickTime
).toLocaleString()
: "Never"}
</div>
</div>
</div>
</div>
</div>
)}
{activeTab === "referrers" && referrersData && (
<div className="space-y-6">
<div className="p-6 border rounded-lg bg-card-bg border-card-border">
<h3 className="mb-4 font-medium text-md text-foreground">
Popular Referrers
</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-card-border">
<thead>
<tr>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Source
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Visits
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Unique Visitors
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Conversion Rate
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Avg. Time Spent
</th>
</tr>
</thead>
<tbody className="divide-y bg-card-bg divide-card-border">
{referrersData.referrers.map(
(referrer: ReferrerItem, i: number) => (
<tr key={i}>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{referrer.source}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{referrer.visitCount} ({referrer.percent}%)
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{referrer.uniqueVisitors}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{referrer.conversionRate}%
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{formatTime(referrer.averageTimeSpent)}
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
</div>
)}
{activeTab === "devices" && deviceData && (
<div className="space-y-6">
<div className="p-6 border rounded-lg bg-card-bg border-card-border">
<h3 className="mb-4 font-medium text-md text-foreground">
Device Types
</h3>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{deviceData.deviceTypes.map(
(device: DeviceItem, i: number) => (
<div key={i} className="flex flex-col items-center">
<div className="mb-2 text-sm font-medium text-text-secondary">
{device.name}
</div>
<div className="text-2xl font-bold text-foreground">
{device.count}
</div>
<div className="mt-1 text-xs text-text-secondary">
{device.percent}%
</div>
</div>
)
)}
</div>
</div>
<div className="p-6 border rounded-lg bg-card-bg border-card-border">
<h3 className="mb-4 font-medium text-md text-foreground">
Device Brands
</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-card-border">
<thead>
<tr>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Brand
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Count
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Percentage
</th>
</tr>
</thead>
<tbody className="divide-y bg-card-bg divide-card-border">
{deviceData.deviceBrands.map(
(brand: DeviceItem, i: number) => (
<tr key={i}>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{brand.name}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{brand.count}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{brand.percent}%
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
</div>
)}
{activeTab === "locations" && platformData && (
<div className="space-y-6">
<div className="p-6 border rounded-lg bg-card-bg border-card-border">
<h3 className="mb-4 font-medium text-md text-foreground">
Platform Distribution
</h3>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<h4 className="mb-3 text-sm font-medium text-text-secondary">
Operating Systems
</h4>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-card-border">
<thead>
<tr>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
OS
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Visits
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Percentage
</th>
</tr>
</thead>
<tbody className="divide-y bg-card-bg divide-card-border">
{platformData.platforms.map((platform, i) => (
<tr key={i}>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{platform.name}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{platform.count}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{platform.percent}%
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div>
<h4 className="mb-3 text-sm font-medium text-text-secondary">
Browsers
</h4>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-card-border">
<thead>
<tr>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Browser
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Visits
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Percentage
</th>
</tr>
</thead>
<tbody className="divide-y bg-card-bg divide-card-border">
{platformData.browsers.map((browser, i) => (
<tr key={i}>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{browser.name}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{browser.count}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{browser.percent}%
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
)}
{activeTab === "qrCodes" && qrCodeData && (
<div className="space-y-6">
<div className="p-6 border rounded-lg bg-card-bg border-card-border">
<h3 className="mb-4 font-medium text-md text-foreground">
QR Code Analysis
</h3>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Total Scans
</div>
<div className="text-2xl font-bold text-foreground">
{qrCodeData.overview.totalScans}
</div>
</div>
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Unique Scanners
</div>
<div className="text-2xl font-bold text-foreground">
{qrCodeData.overview.uniqueScanners}
</div>
</div>
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Conversion Rate
</div>
<div className="text-2xl font-bold text-foreground">
{qrCodeData.overview.conversionRate}%
</div>
</div>
<div className="flex flex-col">
<div className="mb-1 text-sm font-medium text-text-secondary">
Avg. Time Spent
</div>
<div className="text-2xl font-bold text-foreground">
{formatTime(qrCodeData.overview.averageTimeSpent)}
</div>
</div>
</div>
</div>
<div className="p-6 border rounded-lg bg-card-bg border-card-border">
<h3 className="mb-4 font-medium text-md text-foreground">
Scan Locations
</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-card-border">
<thead>
<tr>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Location
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Scans
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Percentage
</th>
</tr>
</thead>
<tbody className="divide-y bg-card-bg divide-card-border">
{qrCodeData.locations.map((location, i) => (
<tr key={i}>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{location.city}, {location.country}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{location.scanCount}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{location.percent}%
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="p-6 border rounded-lg bg-card-bg border-card-border">
<h3 className="mb-4 font-medium text-md text-foreground">
Scan Time Distribution
</h3>
<div className="grid grid-cols-1 gap-6">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-card-border">
<thead>
<tr>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Hour
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Scans
</th>
<th className="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-text-secondary">
Percentage
</th>
</tr>
</thead>
<tbody className="divide-y bg-card-bg divide-card-border">
{qrCodeData.hourlyDistribution.map((hour) => (
<tr key={hour.hour}>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{hour.hour}:00 - {hour.hour}:59
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{hour.scanCount}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap text-foreground">
{hour.percent.toFixed(1)}%
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</>
) : (
<div className="py-8 text-center">
<div className="inline-block p-6 rounded-lg bg-red-500/10">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-10 h-10 mx-auto text-accent-red"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<h2 className="mt-4 text-xl font-bold text-foreground">
Link Not Found
</h2>
<p className="mt-2 text-text-secondary">
The link you're looking for doesn't exist or has been removed.
</p>
<Link
href="/links"
className="inline-block px-4 py-2 mt-4 text-white rounded-lg bg-accent-blue"
>
Return to Links Page
</Link>
</div>
</div>
)}
</div>
</div>
);
}