"use client";
import { useMemo } from "react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
import {
Archive,
BarChart3,
BookOpen,
Clock,
Database,
FileText,
Globe,
Hash,
Heart,
Highlighter,
Image,
Link,
List,
TrendingUp,
} from "lucide-react";
function formatBytes(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
function formatNumber(num: number): string {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + "M";
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + "K";
}
return num.toString();
}
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const hourLabels = Array.from({ length: 24 }, (_, i) =>
i === 0 ? "12 AM" : i < 12 ? `${i} AM` : i === 12 ? "12 PM" : `${i - 12} PM`,
);
function SimpleBarChart({
data,
maxValue,
labels,
}: {
data: number[];
maxValue: number;
labels: string[];
}) {
return (
{data.map((value, index) => (
{labels[index]}
0 ? (value / maxValue) * 100 : 0}%`,
}}
/>
{value}
))}
);
}
function StatCard({
title,
value,
icon,
description,
}: {
title: string;
value: string | number;
icon: React.ReactNode;
description?: string;
}) {
return (
{title}
{icon}
{value}
{description && (
{description}
)}
);
}
export default function StatsPage() {
const { t } = useTranslation();
const { data: stats, isLoading } = api.users.stats.useQuery();
const { data: userSettings } = api.users.settings.useQuery();
const maxHourlyActivity = useMemo(() => {
if (!stats) return 0;
return Math.max(
...stats.bookmarkingActivity.byHour.map(
(h: { hour: number; count: number }) => h.count,
),
);
}, [stats]);
const maxDailyActivity = useMemo(() => {
if (!stats) return 0;
return Math.max(
...stats.bookmarkingActivity.byDayOfWeek.map(
(d: { day: number; count: number }) => d.count,
),
);
}, [stats]);
if (isLoading) {
return (
{t("settings.stats.usage_statistics")}
{t("settings.stats.insights_description")}
{Array.from({ length: 8 }).map((_, i) => (
))}
);
}
if (!stats) {
return (
{t("settings.stats.failed_to_load")}
);
}
return (
{t("settings.stats.usage_statistics")}
Insights into your bookmarking habits and collection
{userSettings?.timezone && userSettings.timezone !== "UTC" && (
Times shown in {userSettings.timezone} timezone
)}
{/* Overview Stats */}
}
description={t("settings.stats.overview.all_saved_items")}
/>
}
description={t("settings.stats.overview.starred_bookmarks")}
/>
}
description={t("settings.stats.overview.archived_items")}
/>
}
description={t("settings.stats.overview.unique_tags_created")}
/>
}
description={t("settings.stats.overview.bookmark_collections")}
/>
}
description={t("settings.stats.overview.text_highlights")}
/>
}
description={t("settings.stats.overview.total_asset_storage")}
/>
}
description={t("settings.stats.overview.bookmarks_added")}
/>
{/* Bookmark Types */}
{t("settings.stats.bookmark_types.title")}
{t("settings.stats.bookmark_types.links")}
{stats.bookmarksByType.link}
0
? (stats.bookmarksByType.link / stats.numBookmarks) * 100
: 0
}
className="h-2"
/>
{t("settings.stats.bookmark_types.text_notes")}
{stats.bookmarksByType.text}
0
? (stats.bookmarksByType.text / stats.numBookmarks) * 100
: 0
}
className="h-2"
/>
{t("settings.stats.bookmark_types.assets")}
{stats.bookmarksByType.asset}
0
? (stats.bookmarksByType.asset / stats.numBookmarks) * 100
: 0
}
className="h-2"
/>
{/* Recent Activity */}
{t("settings.stats.recent_activity.title")}
{stats.bookmarkingActivity.thisWeek}
{t("settings.stats.recent_activity.this_week")}
{stats.bookmarkingActivity.thisMonth}
{t("settings.stats.recent_activity.this_month")}
{stats.bookmarkingActivity.thisYear}
{t("settings.stats.recent_activity.this_year")}
{/* Top Domains */}
{t("settings.stats.top_domains.title")}
{stats.topDomains.length > 0 ? (
{stats.topDomains
.slice(0, 8)
.map(
(
domain: { domain: string; count: number },
index: number,
) => (
{index + 1}
{domain.domain}
{domain.count}
),
)}
) : (
{t("settings.stats.top_domains.no_domains_found")}
)}
{/* Top Tags */}
{t("settings.stats.most_used_tags.title")}
{stats.tagUsage.length > 0 ? (
{stats.tagUsage
.slice(0, 8)
.map(
(tag: { name: string; count: number }, index: number) => (
),
)}
) : (
{t("settings.stats.most_used_tags.no_tags_found")}
)}
{/* Activity Patterns */}
{/* Hourly Activity */}
{t("settings.stats.activity_patterns.activity_by_hour")}
{userSettings?.timezone && userSettings.timezone !== "UTC" && (
({userSettings.timezone})
)}
h.count,
)}
maxValue={maxHourlyActivity}
labels={hourLabels}
/>
{/* Daily Activity */}
{t("settings.stats.activity_patterns.activity_by_day")}
{userSettings?.timezone && userSettings.timezone !== "UTC" && (
({userSettings.timezone})
)}
d.count,
)}
maxValue={maxDailyActivity}
labels={dayNames}
/>
{/* Asset Storage */}
{stats.assetsByType.length > 0 && (
{t("settings.stats.storage_breakdown.title")}
{stats.assetsByType.map(
(asset: { type: string; count: number; totalSize: number }) => (
{asset.type.replace(/([A-Z])/g, " $1").trim()}
{asset.count}
{formatBytes(asset.totalSize)}
0
? (asset.totalSize / stats.totalAssetSize) * 100
: 0
}
className="h-2"
/>
),
)}
)}
);
}