From bdf8ab9fb4be0470cc45e588c9aeb9e724b4a453 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 28 Sep 2025 21:22:33 +0200 Subject: [PATCH] feat: add new dashboard components and enhance UI - Introduced new CSS variables for light theme in `globals.css` to improve visual consistency. - Replaced `Card` component with `StatCard`, `ProgressBar`, and `MetricCard` in `DashboardStats`, `ProductivityAnalytics`, and `RecentTasks` for better modularity and reusability. - Updated `QuickActions` to use `ActionCard` for a more cohesive design. - Enhanced `Badge` and `Button` components with new variants for improved styling options. - Added new UI showcase section in `UIShowcaseClient` to demonstrate the new dashboard components. --- src/app/globals.css | 24 ++ src/components/dashboard/DashboardStats.tsx | 65 ++--- .../dashboard/ProductivityAnalytics.tsx | 61 ++--- src/components/dashboard/QuickActions.tsx | 82 +++--- src/components/dashboard/RecentTasks.tsx | 129 +++------ .../ui-showcase/UIShowcaseClient.tsx | 247 ++++++++++++++++++ src/components/ui/ActionCard.tsx | 65 +++++ src/components/ui/Badge.tsx | 19 +- src/components/ui/Button.tsx | 5 +- src/components/ui/Input.tsx | 2 + src/components/ui/MetricCard.tsx | 78 ++++++ src/components/ui/ProgressBar.tsx | 56 ++++ src/components/ui/StatCard.tsx | 47 ++++ src/components/ui/TaskCard.tsx | 83 ++++++ src/components/ui/index.ts | 7 + src/services/core/user-preferences.ts | 3 +- 16 files changed, 753 insertions(+), 220 deletions(-) create mode 100644 src/components/ui/ActionCard.tsx create mode 100644 src/components/ui/MetricCard.tsx create mode 100644 src/components/ui/ProgressBar.tsx create mode 100644 src/components/ui/StatCard.tsx create mode 100644 src/components/ui/TaskCard.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 892c976..5a0d450 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -24,6 +24,30 @@ --gray-light: #e5e7eb; /* gray-200 */ } +.light { + /* Light theme explicit */ + --background: #f1f5f9; /* slate-100 */ + --foreground: #0f172a; /* slate-900 */ + --card: #ffffff; /* white */ + --card-hover: #f8fafc; /* slate-50 */ + --card-column: #f8fafc; /* slate-50 */ + --border: #cbd5e1; /* slate-300 */ + --input: #ffffff; /* white */ + --primary: #0891b2; /* cyan-600 */ + --primary-foreground: #ffffff; /* white */ + --muted: #94a3b8; /* slate-400 */ + --muted-foreground: #64748b; /* slate-500 */ + --accent: #d97706; /* amber-600 */ + --destructive: #dc2626; /* red-600 */ + --success: #059669; /* emerald-600 */ + --purple: #8b5cf6; /* purple-500 */ + --yellow: #eab308; /* yellow-500 */ + --green: #059669; /* emerald-600 */ + --blue: #2563eb; /* blue-600 */ + --gray: #6b7280; /* gray-500 */ + --gray-light: #e5e7eb; /* gray-200 */ +} + .dark { /* Dark theme override */ --background: #1e293b; /* slate-800 - encore plus clair */ diff --git a/src/components/dashboard/DashboardStats.tsx b/src/components/dashboard/DashboardStats.tsx index 717e834..b001e15 100644 --- a/src/components/dashboard/DashboardStats.tsx +++ b/src/components/dashboard/DashboardStats.tsx @@ -2,6 +2,7 @@ import { TaskStats } from '@/lib/types'; import { Card } from '@/components/ui/Card'; +import { StatCard, ProgressBar } from '@/components/ui'; import { getDashboardStatColors } from '@/lib/status-config'; interface DashboardStatsProps { @@ -18,77 +19,55 @@ export function DashboardStats({ stats }: DashboardStatsProps) { title: 'Total Tâches', value: stats.total, icon: '📋', - type: 'total' as const, - ...getDashboardStatColors('total') + color: 'default' as const }, { title: 'À Faire', value: stats.todo, icon: '⏳', - type: 'todo' as const, - ...getDashboardStatColors('todo') + color: 'warning' as const }, { title: 'En Cours', value: stats.inProgress, icon: '🔄', - type: 'inProgress' as const, - ...getDashboardStatColors('inProgress') + color: 'primary' as const }, { title: 'Terminées', value: stats.completed, icon: '✅', - type: 'completed' as const, - ...getDashboardStatColors('completed') + color: 'success' as const } ]; return (
{statCards.map((stat, index) => ( - -
-
-

- {stat.title} -

-

- {stat.value} -

-
-
- {stat.icon} -
-
-
+ ))} {/* Cartes de pourcentage */}

Taux de Completion

-
- Terminées - {completionRate}% -
-
-
-
+ -
- En Cours - {inProgressRate}% -
-
-
-
+
diff --git a/src/components/dashboard/ProductivityAnalytics.tsx b/src/components/dashboard/ProductivityAnalytics.tsx index 2fb5822..15202f4 100644 --- a/src/components/dashboard/ProductivityAnalytics.tsx +++ b/src/components/dashboard/ProductivityAnalytics.tsx @@ -4,7 +4,7 @@ import { CompletionTrendChart } from '@/components/charts/CompletionTrendChart'; import { VelocityChart } from '@/components/charts/VelocityChart'; import { PriorityDistributionChart } from '@/components/charts/PriorityDistributionChart'; import { WeeklyStatsCard } from '@/components/charts/WeeklyStatsCard'; -import { Card } from '@/components/ui/Card'; +import { Card, MetricCard } from '@/components/ui'; import { DeadlineOverview } from '@/components/deadline/DeadlineOverview'; interface ProductivityAnalyticsProps { @@ -71,42 +71,33 @@ export function ProductivityAnalytics({ metrics, deadlineMetrics }: Productivity

đź’ˇ Insights

-
-
- Vélocité Moyenne -
-
- {metrics.velocityData.length > 0 - ? Math.round(metrics.velocityData.reduce((acc, item) => acc + item.completed, 0) / metrics.velocityData.length) - : 0 - } tâches/sem -
-
+ 0 + ? Math.round(metrics.velocityData.reduce((acc, item) => acc + item.completed, 0) / metrics.velocityData.length) + : 0 + } tâches/sem`} + color="primary" + /> -
-
- Priorité Principale -
-
- {metrics.priorityDistribution.reduce((max, item) => - item.count > max.count ? item : max, - metrics.priorityDistribution[0] - )?.priority || 'N/A'} -
-
+ + item.count > max.count ? item : max, + metrics.priorityDistribution[0] + )?.priority || 'N/A'} + color="success" + /> -
-
- Taux de Completion -
-
- {(() => { - const completed = metrics.statusFlow.find(s => s.status === 'Terminé')?.count || 0; - const total = metrics.statusFlow.reduce((acc, s) => acc + s.count, 0); - return total > 0 ? Math.round((completed / total) * 100) : 0; - })()}% -
-
+ { + const completed = metrics.statusFlow.find(s => s.status === 'Terminé')?.count || 0; + const total = metrics.statusFlow.reduce((acc, s) => acc + s.count, 0); + return total > 0 ? Math.round((completed / total) * 100) : 0; + })()}%`} + color="warning" + />
diff --git a/src/components/dashboard/QuickActions.tsx b/src/components/dashboard/QuickActions.tsx index 7dcf507..18bd0bf 100644 --- a/src/components/dashboard/QuickActions.tsx +++ b/src/components/dashboard/QuickActions.tsx @@ -1,10 +1,9 @@ 'use client'; import { useState } from 'react'; -import { Button } from '@/components/ui/Button'; +import { ActionCard } from '@/components/ui'; import { CreateTaskForm } from '@/components/forms/CreateTaskForm'; import { CreateTaskData } from '@/clients/tasks-client'; -import Link from 'next/link'; interface QuickActionsProps { onCreateTask: (data: CreateTaskData) => Promise; @@ -21,65 +20,54 @@ export function QuickActions({ onCreateTask }: QuickActionsProps) { return ( <>
- + variant="primary" + /> - - - + } + href="/kanban" + variant="secondary" + /> - - - + } + href="/daily" + variant="secondary" + /> - - - + } + href="/settings" + variant="secondary" + />
b.updatedAt.getTime() - a.updatedAt.getTime()) .slice(0, 5); - // Fonctions simplifiées utilisant la configuration centralisée - - const getPriorityStyle = (priority: string) => { - try { - const config = getPriorityConfig(priority as TaskPriority); - const hexColor = getPriorityColorHex(config.color); - return { color: hexColor }; - } catch { - return { color: '#6b7280' }; // gray-500 par défaut - } - }; return ( @@ -56,82 +45,46 @@ export function RecentTasks({ tasks }: RecentTasksProps) { ) : (
{recentTasks.map((task) => ( -
-
-
-
-

{task.title}

- {task.source === 'jira' && ( - - Jira - - )} -
- - {task.description && ( -

- {task.description} -

- )} - -
- - {getStatusLabel(task.status)} - - - {task.priority && ( - - {(() => { - try { - return getPriorityConfig(task.priority as TaskPriority).label; - } catch { - return task.priority; - } - })()} - - )} - - {task.tags && task.tags.length > 0 && ( -
- - {task.tags.length > 2 && ( - - +{task.tags.length - 2} - - )} -
- )} -
-
- -
-
- {formatDateShort(task.updatedAt)} -
- - - - - -
-
-
+ title={task.title} + description={task.description} + status={getStatusLabel(task.status)} + priority={task.priority ? (() => { + try { + return getPriorityConfig(task.priority as TaskPriority).label; + } catch { + return task.priority; + } + })() : undefined} + tags={task.tags && task.tags.length > 0 ? [ + , + ...(task.tags.length > 2 ? [ + + +{task.tags.length - 2} + + ] : []) + ] : undefined} + metadata={formatDateShort(task.updatedAt)} + actions={ + + + + + + } + /> ))}
)} diff --git a/src/components/ui-showcase/UIShowcaseClient.tsx b/src/components/ui-showcase/UIShowcaseClient.tsx index b267813..a5d1066 100644 --- a/src/components/ui-showcase/UIShowcaseClient.tsx +++ b/src/components/ui-showcase/UIShowcaseClient.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/no-unescaped-entities */ 'use client'; import { useState } from 'react'; @@ -7,6 +8,7 @@ import { Alert, AlertTitle, AlertDescription } from '@/components/ui/Alert'; import { Input } from '@/components/ui/Input'; import { StyledCard } from '@/components/ui/StyledCard'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; +import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard } from '@/components/ui'; import { ThemeSelector } from '@/components/ThemeSelector'; export function UIShowcaseClient() { @@ -315,6 +317,251 @@ export function UIShowcaseClient() {
+ {/* Dashboard Components Section */} +
+

+ Dashboard Components +

+ + {/* Stat Cards */} +
+

Stat Cards

+
+
+
+ color="default" +
+ +
+
+
+ color="primary" +
+ +
+
+
+ color="success" +
+ +
+
+
+ color="warning" +
+ +
+
+
+ + {/* Progress Bars */} +
+

Progress Bars

+
+
+
+ color="success" +
+ +
+
+
+ color="primary" +
+ +
+
+
+ color="warning" +
+ +
+
+
+ color="default" +
+ +
+
+
+ + {/* Action Cards */} +
+

Action Cards

+
+
+
+ variant="primary" +
+ + + + } + onClick={() => alert('Action clicked!')} + variant="primary" + /> +
+ +
+
+ variant="secondary" +
+ + + + } + href="#" + variant="secondary" + /> +
+ +
+
+ variant="ghost" +
+ + + + + } + href="#" + variant="ghost" + /> +
+
+
+ + {/* Task Cards */} +
+

Task Cards

+
+ Frontend, + UI + ]} + metadata="Il y a 2h" + actions={ + + } + /> + + Design + ]} + metadata="Hier" + actions={ + + } + /> +
+
+ + {/* Metric Cards */} +
+

Metric Cards

+
+
+
+ color="primary" +
+ +
+ +
+
+ color="success" +
+ +
+ +
+
+ color="warning" +
+ +
+
+
+
+ {/* Footer */}

diff --git a/src/components/ui/ActionCard.tsx b/src/components/ui/ActionCard.tsx new file mode 100644 index 0000000..b89d82d --- /dev/null +++ b/src/components/ui/ActionCard.tsx @@ -0,0 +1,65 @@ +import { ReactNode } from 'react'; +import { Button } from './Button'; +import { cn } from '@/lib/utils'; + +interface ActionCardProps { + title: string; + description?: string; + icon?: ReactNode; + onClick?: () => void; + href?: string; + variant?: 'primary' | 'secondary' | 'ghost'; + className?: string; +} + +export function ActionCard({ + title, + description, + icon, + onClick, + href, + variant = 'secondary', + className +}: ActionCardProps) { + const content = ( + + ); + + if (href) { + return ( + + {content} + + ); + } + + return content; +} diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx index 527609f..298ee90 100644 --- a/src/components/ui/Badge.tsx +++ b/src/components/ui/Badge.tsx @@ -2,11 +2,12 @@ import { HTMLAttributes, forwardRef } from 'react'; import { cn } from '@/lib/utils'; interface BadgeProps extends HTMLAttributes { - variant?: 'default' | 'primary' | 'success' | 'destructive' | 'accent' | 'purple' | 'yellow' | 'green' | 'blue' | 'gray'; + variant?: 'default' | 'primary' | 'success' | 'destructive' | 'accent' | 'purple' | 'yellow' | 'green' | 'blue' | 'gray' | 'outline' | 'danger' | 'warning'; + size?: 'sm' | 'md' | 'lg'; } const Badge = forwardRef( - ({ className, variant = 'default', ...props }, ref) => { + ({ className, variant = 'default', size = 'md', ...props }, ref) => { const variants = { default: 'bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)]', primary: 'bg-[color-mix(in_srgb,var(--primary)_10%,transparent)] text-[var(--primary)] border border-[color-mix(in_srgb,var(--primary)_25%,var(--border))]', @@ -17,15 +18,25 @@ const Badge = forwardRef( yellow: 'bg-[color-mix(in_srgb,var(--yellow)_10%,transparent)] text-[var(--yellow)] border border-[color-mix(in_srgb,var(--yellow)_25%,var(--border))]', green: 'bg-[color-mix(in_srgb,var(--green)_10%,transparent)] text-[var(--green)] border border-[color-mix(in_srgb,var(--green)_25%,var(--border))]', blue: 'bg-[color-mix(in_srgb,var(--blue)_10%,transparent)] text-[var(--blue)] border border-[color-mix(in_srgb,var(--blue)_25%,var(--border))]', - gray: 'bg-[color-mix(in_srgb,var(--gray)_10%,transparent)] text-[var(--gray)] border border-[color-mix(in_srgb,var(--gray)_25%,var(--border))]' + gray: 'bg-[color-mix(in_srgb,var(--gray)_10%,transparent)] text-[var(--gray)] border border-[color-mix(in_srgb,var(--gray)_25%,var(--border))]', + outline: 'bg-transparent text-[var(--foreground)] border border-[var(--border)]', + danger: 'bg-[color-mix(in_srgb,var(--destructive)_10%,transparent)] text-[var(--destructive)] border border-[color-mix(in_srgb,var(--destructive)_25%,var(--border))]', + warning: 'bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] text-[var(--accent)] border border-[color-mix(in_srgb,var(--accent)_25%,var(--border))]' + }; + + const sizes = { + sm: 'px-2 py-0.5 text-xs', + md: 'px-2.5 py-0.5 text-xs', + lg: 'px-3 py-1 text-sm' }; return (

{ - variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'success' | 'selected'; + variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'success' | 'selected' | 'danger'; size?: 'sm' | 'md' | 'lg'; } @@ -14,7 +14,8 @@ const Button = forwardRef( ghost: 'text-[var(--foreground)] hover:bg-[var(--card-hover)]', destructive: 'bg-[var(--destructive)] text-white hover:bg-[color-mix(in_srgb,var(--destructive)_90%,transparent)]', success: 'bg-[var(--success)] text-white hover:bg-[color-mix(in_srgb,var(--success)_90%,transparent)]', - selected: 'bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)] hover:bg-[color-mix(in_srgb,var(--primary)_20%,transparent)]' + selected: 'bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)] hover:bg-[color-mix(in_srgb,var(--primary)_20%,transparent)]', + danger: 'bg-[var(--destructive)] text-white hover:bg-[color-mix(in_srgb,var(--destructive)_90%,transparent)]' }; const sizes = { diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 6d50d7d..c311bf7 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -3,6 +3,8 @@ import { cn } from '@/lib/utils'; interface InputProps extends InputHTMLAttributes { variant?: 'default' | 'error'; + label?: string; + error?: string; } const Input = forwardRef( diff --git a/src/components/ui/MetricCard.tsx b/src/components/ui/MetricCard.tsx new file mode 100644 index 0000000..d03bb6f --- /dev/null +++ b/src/components/ui/MetricCard.tsx @@ -0,0 +1,78 @@ +import { ReactNode } from 'react'; +import { Card } from './Card'; +import { cn } from '@/lib/utils'; + +interface MetricCardProps { + title: string; + value: string | number; + subtitle?: string; + icon?: ReactNode; + color?: 'default' | 'primary' | 'success' | 'warning' | 'destructive'; + className?: string; +} + +const colorVariants = { + default: { + title: 'text-[var(--foreground)]', + value: 'text-[var(--foreground)]', + border: 'border-[var(--border)] hover:border-[var(--primary)]/50' + }, + primary: { + title: 'text-[var(--primary)]', + value: 'text-[var(--foreground)]', + border: 'border-[var(--border)] hover:border-[var(--primary)]/50' + }, + success: { + title: 'text-[var(--success)]', + value: 'text-[var(--foreground)]', + border: 'border-[var(--border)] hover:border-[var(--success)]/50' + }, + warning: { + title: 'text-[var(--accent)]', + value: 'text-[var(--foreground)]', + border: 'border-[var(--border)] hover:border-[var(--accent)]/50' + }, + destructive: { + title: 'text-[var(--destructive)]', + value: 'text-[var(--foreground)]', + border: 'border-[var(--border)] hover:border-[var(--destructive)]/50' + } +}; + +export function MetricCard({ + title, + value, + subtitle, + icon, + color = 'default', + className +}: MetricCardProps) { + const colors = colorVariants[color]; + + return ( + +
+
+ {title} +
+
+ {value} +
+ {subtitle && ( +
+ {subtitle} +
+ )} + {icon && ( +
+ {icon} +
+ )} +
+
+ ); +} diff --git a/src/components/ui/ProgressBar.tsx b/src/components/ui/ProgressBar.tsx new file mode 100644 index 0000000..ff15d02 --- /dev/null +++ b/src/components/ui/ProgressBar.tsx @@ -0,0 +1,56 @@ +import { cn } from '@/lib/utils'; + +interface ProgressBarProps { + value: number; // 0-100 + label?: string; + color?: 'default' | 'primary' | 'success' | 'warning' | 'destructive'; + showPercentage?: boolean; + className?: string; +} + +const colorVariants = { + default: 'bg-[var(--primary)]', + primary: 'bg-[var(--primary)]', + success: 'bg-[var(--success)]', + warning: 'bg-[var(--accent)]', + destructive: 'bg-[var(--destructive)]' +}; + +export function ProgressBar({ + value, + label, + color = 'default', + showPercentage = true, + className +}: ProgressBarProps) { + const clampedValue = Math.min(Math.max(value, 0), 100); + + return ( +
+ {(label || showPercentage) && ( +
+ {label && ( + + {label} + + )} + {showPercentage && ( + + {Math.round(clampedValue)}% + + )} +
+ )} + +
+
+
+
+ ); +} diff --git a/src/components/ui/StatCard.tsx b/src/components/ui/StatCard.tsx new file mode 100644 index 0000000..a37457f --- /dev/null +++ b/src/components/ui/StatCard.tsx @@ -0,0 +1,47 @@ +import { ReactNode } from 'react'; +import { Card } from './Card'; +import { cn } from '@/lib/utils'; + +interface StatCardProps { + title: string; + value: number | string; + icon?: ReactNode; + color?: 'default' | 'primary' | 'success' | 'warning' | 'destructive'; + className?: string; +} + +const colorVariants = { + default: 'text-[var(--foreground)]', + primary: 'text-[var(--primary)]', + success: 'text-[var(--success)]', + warning: 'text-[var(--accent)]', + destructive: 'text-[var(--destructive)]' +}; + +export function StatCard({ + title, + value, + icon, + color = 'default', + className +}: StatCardProps) { + return ( + +
+
+

+ {title} +

+

+ {value} +

+
+ {icon && ( +
+ {icon} +
+ )} +
+
+ ); +} diff --git a/src/components/ui/TaskCard.tsx b/src/components/ui/TaskCard.tsx new file mode 100644 index 0000000..36dc8f2 --- /dev/null +++ b/src/components/ui/TaskCard.tsx @@ -0,0 +1,83 @@ +import { ReactNode } from 'react'; +import { Badge } from './Badge'; +import { cn } from '@/lib/utils'; + +interface TaskCardProps { + title: string; + description?: string; + status?: string; + priority?: string; + tags?: ReactNode[]; + metadata?: ReactNode; + actions?: ReactNode; + className?: string; +} + +export function TaskCard({ + title, + description, + status, + priority, + tags, + metadata, + actions, + className +}: TaskCardProps) { + return ( +
+
+
+
+

+ {title} +

+
+ + {description && ( +

+ {description} +

+ )} + +
+ {status && ( + + {status} + + )} + + {priority && ( + + {priority} + + )} + + {tags && tags.length > 0 && ( +
+ {tags} +
+ )} +
+
+ +
+ {metadata && ( +
+ {metadata} +
+ )} + {actions && ( +
+ {actions} +
+ )} +
+
+
+ ); +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index d54f3bd..4adc60b 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -5,6 +5,13 @@ export { Alert, AlertTitle, AlertDescription } from './Alert'; export { Input } from './Input'; export { StyledCard } from './StyledCard'; +// Composants Dashboard +export { StatCard } from './StatCard'; +export { ProgressBar } from './ProgressBar'; +export { ActionCard } from './ActionCard'; +export { TaskCard } from './TaskCard'; +export { MetricCard } from './MetricCard'; + // Composants existants export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card'; export { Header } from './Header'; diff --git a/src/services/core/user-preferences.ts b/src/services/core/user-preferences.ts index ee65e99..4649573 100644 --- a/src/services/core/user-preferences.ts +++ b/src/services/core/user-preferences.ts @@ -5,6 +5,7 @@ import { ColumnVisibility, UserPreferences, JiraConfig, + Theme, } from '@/lib/types'; import { TfsConfig } from '@/services/integrations/tfs'; import { prisma } from './database'; @@ -213,7 +214,7 @@ class UserPreferencesService { /** * Récupère uniquement le thème pour le SSR (optimisé) */ - async getTheme(): Promise<'light' | 'dark'> { + async getTheme(): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); const viewPrefs = userPrefs.viewPreferences as ViewPreferences;