Initial commit: Pixel AI comic/video creation platform
- FastAPI backend with SQLModel, Alembic migrations, AgentScope agents - Next.js 15 frontend with React 19, Tailwind, Zustand, React Flow - Multi-provider AI system (DashScope, Kling, MiniMax, Volcengine, OpenAI, etc.) - All HTTP clients migrated from sync requests to async httpx - Admin-managed API keys via environment variables - SSRF vulnerability fixed in ensure_url()
This commit is contained in:
81
frontend/src/components/admin/common/PageStats.tsx
Normal file
81
frontend/src/components/admin/common/PageStats.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface PageStatsProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
columns?: 2 | 3 | 4 | 5 | 6;
|
||||
}
|
||||
|
||||
export function PageStats({ children, className, columns = 4 }: PageStatsProps) {
|
||||
const columnClasses = {
|
||||
2: 'grid-cols-1 sm:grid-cols-2',
|
||||
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
||||
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
|
||||
5: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-5',
|
||||
6: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('grid gap-4', columnClasses[columns], className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageStatCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
icon?: ReactNode;
|
||||
description?: string;
|
||||
loading?: boolean;
|
||||
trend?: {
|
||||
value: number;
|
||||
label: string;
|
||||
positive?: boolean;
|
||||
};
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PageStatCard({
|
||||
title,
|
||||
value,
|
||||
icon,
|
||||
description,
|
||||
loading,
|
||||
trend,
|
||||
className,
|
||||
}: PageStatCardProps) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
{icon && <div className="text-muted-foreground">{icon}</div>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<Skeleton className="h-8 w-24" />
|
||||
) : (
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
)}
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground mt-1">{description}</p>
|
||||
)}
|
||||
{trend && (
|
||||
<p className="text-xs mt-1">
|
||||
<span className={trend.positive ? 'text-green-500' : 'text-red-500'}>
|
||||
{trend.positive ? '↑' : '↓'} {Math.abs(trend.value)}%
|
||||
</span>
|
||||
{' '}{trend.label}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageStats;
|
||||
Reference in New Issue
Block a user