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:
@@ -9,6 +9,9 @@ import { Header } from '@/components/ui/Header';
|
|||||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import { PeriodSelector, SkeletonGrid, MetricsGrid } from '@/components/ui';
|
||||||
|
import { AlertBanner } from '@/components/ui/AlertBanner';
|
||||||
|
import { Tabs } from '@/components/ui/Tabs';
|
||||||
import { VelocityChart } from '@/components/jira/VelocityChart';
|
import { VelocityChart } from '@/components/jira/VelocityChart';
|
||||||
import { TeamDistributionChart } from '@/components/jira/TeamDistributionChart';
|
import { TeamDistributionChart } from '@/components/jira/TeamDistributionChart';
|
||||||
import { CycleTimeChart } from '@/components/jira/CycleTimeChart';
|
import { CycleTimeChart } from '@/components/jira/CycleTimeChart';
|
||||||
@@ -197,26 +200,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
|
|||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Sélecteur de période */}
|
{/* Sélecteur de période */}
|
||||||
<div className="flex bg-[var(--card)] border border-[var(--border)] rounded-lg p-1">
|
<PeriodSelector
|
||||||
{[
|
options={[
|
||||||
{ value: '7d', label: '7j' },
|
{ value: '7d', label: '7j' },
|
||||||
{ value: '30d', label: '30j' },
|
{ value: '30d', label: '30j' },
|
||||||
{ value: '3m', label: '3m' },
|
{ value: '3m', label: '3m' },
|
||||||
{ value: 'current', label: 'Sprint' }
|
{ value: 'current', label: 'Sprint' }
|
||||||
].map((period: { value: string; label: string }) => (
|
]}
|
||||||
<button
|
selectedValue={selectedPeriod}
|
||||||
key={period.value}
|
onValueChange={(value) => setSelectedPeriod(value as PeriodFilter)}
|
||||||
onClick={() => setSelectedPeriod(period.value as PeriodFilter)}
|
/>
|
||||||
className={`px-3 py-1 text-sm rounded transition-all ${
|
|
||||||
selectedPeriod === period.value
|
|
||||||
? 'bg-[var(--primary)] text-[var(--primary-foreground)]'
|
|
||||||
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{period.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{analytics && (
|
{analytics && (
|
||||||
@@ -260,40 +253,27 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
|
|||||||
|
|
||||||
{/* Contenu principal */}
|
{/* Contenu principal */}
|
||||||
{error && (
|
{error && (
|
||||||
<Card className="mb-6 border-red-500/20 bg-red-500/10">
|
<AlertBanner
|
||||||
<CardContent className="p-4">
|
title="Erreur"
|
||||||
<div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
|
items={[{ id: 'error', title: error }]}
|
||||||
<span>❌</span>
|
icon="❌"
|
||||||
<span>{error}</span>
|
variant="error"
|
||||||
</div>
|
className="mb-6"
|
||||||
</CardContent>
|
/>
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{exportError && (
|
{exportError && (
|
||||||
<Card className="mb-6 border-orange-500/20 bg-orange-500/10">
|
<AlertBanner
|
||||||
<CardContent className="p-4">
|
title="Erreur d'export"
|
||||||
<div className="flex items-center gap-2" style={{ color: 'var(--accent)' }}>
|
items={[{ id: 'export-error', title: exportError }]}
|
||||||
<span>⚠️</span>
|
icon="⚠️"
|
||||||
<span>Erreur d'export: {exportError}</span>
|
variant="warning"
|
||||||
</div>
|
className="mb-6"
|
||||||
</CardContent>
|
/>
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading && !analytics && (
|
{isLoading && !analytics && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<SkeletonGrid count={6} />
|
||||||
{/* Skeleton loading */}
|
|
||||||
{[1, 2, 3, 4, 5, 6].map(i => (
|
|
||||||
<Card key={i} className="animate-pulse">
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="h-4 bg-[var(--muted)] rounded mb-4"></div>
|
|
||||||
<div className="h-8 bg-[var(--muted)] rounded mb-2"></div>
|
|
||||||
<div className="h-4 bg-[var(--muted)] rounded w-2/3"></div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{analytics && (
|
{analytics && (
|
||||||
@@ -313,40 +293,30 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<MetricsGrid
|
||||||
<div className="text-center">
|
metrics={[
|
||||||
<div className="text-xl font-bold text-[var(--primary)]">
|
{
|
||||||
{analytics.project.totalIssues}
|
title: 'Tickets',
|
||||||
</div>
|
value: analytics.project.totalIssues,
|
||||||
<div className="text-xs text-[var(--muted-foreground)]">
|
color: 'primary'
|
||||||
Tickets
|
},
|
||||||
</div>
|
{
|
||||||
</div>
|
title: 'Équipe',
|
||||||
<div className="text-center">
|
value: analytics.teamMetrics.totalAssignees,
|
||||||
<div className="text-xl font-bold text-blue-500">
|
color: 'default'
|
||||||
{analytics.teamMetrics.totalAssignees}
|
},
|
||||||
</div>
|
{
|
||||||
<div className="text-xs text-[var(--muted-foreground)]">
|
title: 'Actifs',
|
||||||
Équipe
|
value: analytics.teamMetrics.activeAssignees,
|
||||||
</div>
|
color: 'success'
|
||||||
</div>
|
},
|
||||||
<div className="text-center">
|
{
|
||||||
<div className="text-xl font-bold text-green-500">
|
title: 'Points',
|
||||||
{analytics.teamMetrics.activeAssignees}
|
value: analytics.velocityMetrics.currentSprintPoints,
|
||||||
</div>
|
color: 'warning'
|
||||||
<div className="text-xs text-[var(--muted-foreground)]">
|
}
|
||||||
Actifs
|
]}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-xl font-bold text-orange-500">
|
|
||||||
{analytics.velocityMetrics.currentSprintPoints}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--muted-foreground)]">
|
|
||||||
Points
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -363,28 +333,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
|
|||||||
<AnomalyDetectionPanel />
|
<AnomalyDetectionPanel />
|
||||||
|
|
||||||
{/* Onglets de navigation */}
|
{/* Onglets de navigation */}
|
||||||
<div className="border-b border-[var(--border)]">
|
<Tabs
|
||||||
<nav className="flex space-x-8">
|
items={[
|
||||||
{[
|
{ id: 'overview', label: '📊 Vue d\'ensemble' },
|
||||||
{ id: 'overview', label: '📊 Vue d\'ensemble' },
|
{ id: 'velocity', label: '🚀 Vélocité & Sprints' },
|
||||||
{ id: 'velocity', label: '🚀 Vélocité & Sprints' },
|
{ id: 'analytics', label: '📈 Analytics avancées' },
|
||||||
{ id: 'analytics', label: '📈 Analytics avancées' },
|
{ id: 'quality', label: '🎯 Qualité & Collaboration' }
|
||||||
{ id: 'quality', label: '🎯 Qualité & Collaboration' }
|
]}
|
||||||
].map(tab => (
|
activeTab={activeTab}
|
||||||
<button
|
onTabChange={(tabId) => setActiveTab(tabId as 'overview' | 'velocity' | 'analytics' | 'quality')}
|
||||||
key={tab.id}
|
/>
|
||||||
onClick={() => setActiveTab(tab.id as 'overview' | 'velocity' | 'analytics' | 'quality')}
|
|
||||||
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
|
||||||
activeTab === tab.id
|
|
||||||
? 'border-[var(--primary)] text-[var(--primary)]'
|
|
||||||
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:border-[var(--border)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contenu des onglets */}
|
{/* Contenu des onglets */}
|
||||||
{activeTab === 'overview' && (
|
{activeTab === 'overview' && (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Alert as ShadcnAlert, AlertTitle, AlertDescription } from '@/components
|
|||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { StyledCard } from '@/components/ui/StyledCard';
|
import { StyledCard } from '@/components/ui/StyledCard';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip, ColumnHeader, EmptyState, DropZone, Tabs, PriorityBadge, AchievementCard, ChallengeCard } from '@/components/ui';
|
import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip, ColumnHeader, EmptyState, DropZone, Tabs, PriorityBadge, AchievementCard, ChallengeCard, PeriodSelector, SkeletonCard, SkeletonGrid, MetricsGrid } from '@/components/ui';
|
||||||
import { CheckboxItem, CheckboxItemData } from '@/components/ui/CheckboxItem';
|
import { CheckboxItem, CheckboxItemData } from '@/components/ui/CheckboxItem';
|
||||||
import { Calendar } from '@/components/ui/Calendar';
|
import { Calendar } from '@/components/ui/Calendar';
|
||||||
import { DailyAddForm } from '@/components/ui/DailyAddForm';
|
import { DailyAddForm } from '@/components/ui/DailyAddForm';
|
||||||
@@ -22,6 +22,7 @@ import { ChallengeData } from '@/components/ui/ChallengeCard';
|
|||||||
export function UIShowcaseClient() {
|
export function UIShowcaseClient() {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
|
const [selectedPeriod, setSelectedPeriod] = useState('7d');
|
||||||
const [checkboxItems, setCheckboxItems] = useState<CheckboxItemData[]>([
|
const [checkboxItems, setCheckboxItems] = useState<CheckboxItemData[]>([
|
||||||
{ id: '1', text: 'Tâche complétée', isChecked: true, type: 'task' },
|
{ id: '1', text: 'Tâche complétée', isChecked: true, type: 'task' },
|
||||||
{ id: '2', text: 'Réunion importante', isChecked: false, type: 'meeting' },
|
{ id: '2', text: 'Réunion importante', isChecked: false, type: 'meeting' },
|
||||||
@@ -1081,6 +1082,93 @@ export function UIShowcaseClient() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Jira Dashboard Components Section */}
|
||||||
|
<section className="space-y-8">
|
||||||
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
|
Jira Dashboard Components
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
{/* PeriodSelector */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">PeriodSelector</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
PeriodSelector - Sélecteur de période
|
||||||
|
</div>
|
||||||
|
<PeriodSelector
|
||||||
|
options={[
|
||||||
|
{ value: '7d', label: '7j' },
|
||||||
|
{ value: '30d', label: '30j' },
|
||||||
|
{ value: '3m', label: '3m' },
|
||||||
|
{ value: 'current', label: 'Sprint' }
|
||||||
|
]}
|
||||||
|
selectedValue={selectedPeriod}
|
||||||
|
onValueChange={setSelectedPeriod}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-[var(--muted-foreground)]">
|
||||||
|
Période sélectionnée: {selectedPeriod}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* MetricsGrid */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">MetricsGrid</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
MetricsGrid - Grille de métriques
|
||||||
|
</div>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<MetricsGrid
|
||||||
|
metrics={[
|
||||||
|
{ title: 'Tickets', value: 42, color: 'primary' },
|
||||||
|
{ title: 'Équipe', value: 8, color: 'default' },
|
||||||
|
{ title: 'Actifs', value: 6, color: 'success' },
|
||||||
|
{ title: 'Points', value: 156, color: 'warning' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SkeletonCard */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">SkeletonCard</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
SkeletonCard - Carte de chargement
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<SkeletonCard lines={3} />
|
||||||
|
<SkeletonCard lines={4} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SkeletonGrid */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">SkeletonGrid</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
SkeletonGrid - Grille de chargement
|
||||||
|
</div>
|
||||||
|
<SkeletonGrid count={4} lines={3} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Daily Components Section */}
|
{/* Daily Components Section */}
|
||||||
<section className="space-y-8">
|
<section className="space-y-8">
|
||||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
|
|||||||
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 { AlertBanner } from './AlertBanner';
|
||||||
export { CollapsibleSection } from './CollapsibleSection';
|
export { CollapsibleSection } from './CollapsibleSection';
|
||||||
|
|
||||||
|
// Composants Jira Dashboard
|
||||||
|
export { PeriodSelector } from './PeriodSelector';
|
||||||
|
export { SkeletonCard, SkeletonGrid } from './SkeletonCard';
|
||||||
|
export { MetricsGrid } from './MetricsGrid';
|
||||||
|
|
||||||
// Composants existants
|
// Composants existants
|
||||||
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
|
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
|
||||||
export { FontSizeToggle } from './FontSizeToggle';
|
export { FontSizeToggle } from './FontSizeToggle';
|
||||||
|
|||||||
Reference in New Issue
Block a user