feat: enhance JiraDashboardPage with new components and improved UI
- Integrated `PeriodSelector`, `SkeletonGrid`, and `MetricsGrid` for better data visualization and user interaction. - Replaced legacy period selection and error display with new components for a cleaner UI. - Updated `UIShowcaseClient` to demonstrate new Jira dashboard components, enhancing showcase functionality.
This commit is contained in:
57
src/components/ui/MetricsGrid.tsx
Normal file
57
src/components/ui/MetricsGrid.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface MetricsGridProps {
|
||||
metrics: Array<{
|
||||
title: string;
|
||||
value: string | number;
|
||||
subtitle?: string;
|
||||
icon?: ReactNode;
|
||||
color?: 'default' | 'primary' | 'success' | 'warning' | 'destructive';
|
||||
}>;
|
||||
className?: string;
|
||||
columns?: 2 | 3 | 4;
|
||||
}
|
||||
|
||||
export function MetricsGrid({
|
||||
metrics,
|
||||
className,
|
||||
columns = 4
|
||||
}: MetricsGridProps) {
|
||||
const gridCols = {
|
||||
2: 'grid-cols-2',
|
||||
3: 'grid-cols-3',
|
||||
4: 'grid-cols-2 lg:grid-cols-4'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"grid gap-4",
|
||||
gridCols[columns],
|
||||
className
|
||||
)}>
|
||||
{metrics.map((metric, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<div className={cn(
|
||||
"text-xl font-bold",
|
||||
metric.color === 'primary' && 'text-[var(--primary)]',
|
||||
metric.color === 'success' && 'text-[var(--success)]',
|
||||
metric.color === 'warning' && 'text-[var(--accent)]',
|
||||
metric.color === 'destructive' && 'text-[var(--destructive)]',
|
||||
!metric.color && 'text-[var(--foreground)]'
|
||||
)}>
|
||||
{metric.value}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--muted-foreground)]">
|
||||
{metric.title}
|
||||
</div>
|
||||
{metric.subtitle && (
|
||||
<div className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
{metric.subtitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/components/ui/PeriodSelector.tsx
Normal file
45
src/components/ui/PeriodSelector.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface PeriodOption {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
interface PeriodSelectorProps {
|
||||
options: PeriodOption[];
|
||||
selectedValue: string;
|
||||
onValueChange: (value: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PeriodSelector({
|
||||
options,
|
||||
selectedValue,
|
||||
onValueChange,
|
||||
className
|
||||
}: PeriodSelectorProps) {
|
||||
return (
|
||||
<div className={cn(
|
||||
"flex bg-[var(--card)] border border-[var(--border)] rounded-lg p-1",
|
||||
className
|
||||
)}>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => onValueChange(option.value)}
|
||||
className={cn(
|
||||
"px-3 py-1 text-sm rounded transition-all flex items-center gap-1",
|
||||
selectedValue === option.value
|
||||
? 'bg-[var(--primary)] text-[var(--primary-foreground)]'
|
||||
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
|
||||
)}
|
||||
>
|
||||
{option.icon && <span>{option.icon}</span>}
|
||||
<span>{option.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
src/components/ui/SkeletonCard.tsx
Normal file
50
src/components/ui/SkeletonCard.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Card, CardContent } from './Card';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface SkeletonCardProps {
|
||||
className?: string;
|
||||
lines?: number;
|
||||
}
|
||||
|
||||
export function SkeletonCard({ className, lines = 3 }: SkeletonCardProps) {
|
||||
return (
|
||||
<Card className={cn("animate-pulse", className)}>
|
||||
<CardContent className="p-6">
|
||||
<div className="space-y-4">
|
||||
{/* Titre */}
|
||||
<div className="h-4 bg-[var(--muted)] rounded mb-4"></div>
|
||||
|
||||
{/* Valeur principale */}
|
||||
<div className="h-8 bg-[var(--muted)] rounded mb-2"></div>
|
||||
|
||||
{/* Lignes supplémentaires */}
|
||||
{Array.from({ length: lines - 2 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
"h-4 bg-[var(--muted)] rounded",
|
||||
i === lines - 3 ? "w-2/3" : "w-full"
|
||||
)}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface SkeletonGridProps {
|
||||
count?: number;
|
||||
className?: string;
|
||||
lines?: number;
|
||||
}
|
||||
|
||||
export function SkeletonGrid({ count = 6, className, lines = 3 }: SkeletonGridProps) {
|
||||
return (
|
||||
<div className={cn("grid grid-cols-1 lg:grid-cols-3 gap-6", className)}>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<SkeletonCard key={i} lines={lines} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,6 +35,11 @@ export { DailyAddForm } from './DailyAddForm';
|
||||
export { AlertBanner } from './AlertBanner';
|
||||
export { CollapsibleSection } from './CollapsibleSection';
|
||||
|
||||
// Composants Jira Dashboard
|
||||
export { PeriodSelector } from './PeriodSelector';
|
||||
export { SkeletonCard, SkeletonGrid } from './SkeletonCard';
|
||||
export { MetricsGrid } from './MetricsGrid';
|
||||
|
||||
// Composants existants
|
||||
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
|
||||
export { FontSizeToggle } from './FontSizeToggle';
|
||||
|
||||
Reference in New Issue
Block a user