Files
stack/apps/web/src/components/knowledge/StatsDashboard.tsx
Jason Woltje ac1f2c176f
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: Resolve all ESLint errors and warnings in web package
Fixes all 542 ESLint problems in the web package to achieve 0 errors and 0 warnings.

Changes:
- Fixed 144 issues: nullish coalescing, return types, unused variables
- Fixed 118 issues: unnecessary conditions, type safety, template literals
- Fixed 79 issues: non-null assertions, unsafe assignments, empty functions
- Fixed 67 issues: explicit return types, promise handling, enum comparisons
- Fixed 45 final warnings: missing return types, optional chains
- Fixed 25 typecheck-related issues: async/await, type assertions, formatting
- Fixed JSX.Element namespace errors across 90+ files

All Quality Rails violations resolved. Lint and typecheck both pass with 0 problems.

Files modified: 118 components, tests, hooks, and utilities

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-31 00:10:03 -06:00

246 lines
8.2 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { fetchKnowledgeStats } from "@/lib/api/knowledge";
import Link from "next/link";
interface KnowledgeStats {
overview: {
totalEntries: number;
totalTags: number;
totalLinks: number;
publishedEntries: number;
draftEntries: number;
archivedEntries: number;
};
mostConnected: {
id: string;
slug: string;
title: string;
incomingLinks: number;
outgoingLinks: number;
totalConnections: number;
}[];
recentActivity: {
id: string;
slug: string;
title: string;
updatedAt: string;
status: string;
}[];
tagDistribution: {
id: string;
name: string;
slug: string;
color: string | null;
entryCount: number;
}[];
}
export function StatsDashboard(): React.JSX.Element {
const [stats, setStats] = useState<KnowledgeStats | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadStats(): Promise<void> {
try {
setIsLoading(true);
const data = await fetchKnowledgeStats();
setStats(data as KnowledgeStats);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load statistics");
} finally {
setIsLoading(false);
}
}
void loadStats();
}, []);
if (isLoading) {
return (
<div className="flex items-center justify-center p-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
</div>
);
}
if (error || !stats) {
return <div className="p-8 text-center text-red-500">Error loading statistics: {error}</div>;
}
const { overview, mostConnected, recentActivity, tagDistribution } = stats;
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
Knowledge Base Statistics
</h1>
<Link
href="/knowledge"
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
Back to Entries
</Link>
</div>
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StatsCard
title="Total Entries"
value={overview.totalEntries}
subtitle={`${String(overview.publishedEntries)} published • ${String(overview.draftEntries)} drafts`}
icon="📚"
/>
<StatsCard
title="Tags"
value={overview.totalTags}
subtitle="Organize your knowledge"
icon="🏷️"
/>
<StatsCard
title="Connections"
value={overview.totalLinks}
subtitle="Links between entries"
icon="🔗"
/>
</div>
{/* Two Column Layout */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Most Connected Entries */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Most Connected Entries
</h2>
<div className="space-y-3">
{mostConnected.slice(0, 10).map((entry) => (
<div
key={entry.id}
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
>
<div className="flex-1 min-w-0">
<Link
href={`/knowledge/${entry.slug}`}
className="text-sm font-medium text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 truncate block"
>
{entry.title}
</Link>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{entry.incomingLinks} incoming {entry.outgoingLinks} outgoing
</div>
</div>
<div className="ml-4 flex-shrink-0">
<span className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 font-semibold text-sm">
{entry.totalConnections}
</span>
</div>
</div>
))}
{mostConnected.length === 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
No connections yet
</p>
)}
</div>
</div>
{/* Recent Activity */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Recent Activity
</h2>
<div className="space-y-3">
{recentActivity.slice(0, 10).map((entry) => (
<div
key={entry.id}
className="flex items-start justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
>
<div className="flex-1 min-w-0">
<Link
href={`/knowledge/${entry.slug}`}
className="text-sm font-medium text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 truncate block"
>
{entry.title}
</Link>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{new Date(entry.updatedAt).toLocaleDateString()} {" "}
<span className="capitalize">{entry.status.toLowerCase()}</span>
</div>
</div>
</div>
))}
{recentActivity.length === 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
No recent activity
</p>
)}
</div>
</div>
</div>
{/* Tag Distribution */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
Tag Distribution
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{tagDistribution.slice(0, 12).map((tag) => (
<div
key={tag.id}
className="flex items-center justify-between p-3 rounded bg-gray-50 dark:bg-gray-700"
>
<div className="flex items-center gap-2 flex-1 min-w-0">
{tag.color && (
<div
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: tag.color }}
/>
)}
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{tag.name}
</span>
</div>
<span className="ml-2 text-xs font-semibold text-gray-600 dark:text-gray-400 flex-shrink-0">
{tag.entryCount}
</span>
</div>
))}
{tagDistribution.length === 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400 col-span-full text-center py-4">
No tags yet
</p>
)}
</div>
</div>
</div>
);
}
function StatsCard({
title,
value,
subtitle,
icon,
}: {
title: string;
value: number;
subtitle: string;
icon: string;
}): React.JSX.Element {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{title}</p>
<p className="text-3xl font-bold text-gray-900 dark:text-gray-100 mt-2">{value}</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{subtitle}</p>
</div>
<div className="text-4xl">{icon}</div>
</div>
</div>
);
}