feat: add line clamp utility and integrate RecentTaskTimeline component

- Added a new CSS utility for line clamping to `globals.css` for better text overflow handling.
- Integrated `WelcomeSection` into `HomePageClient` for enhanced user experience.
- Replaced `TaskCard` with `RecentTaskTimeline` in `RecentTasks` for improved task visualization.
- Updated `ui/index.ts` to export `RecentTaskTimeline` and showcased it in `CardsSection` and `FeedbackSection`.
This commit is contained in:
Julien Froidefond
2025-09-30 23:34:03 +02:00
parent d8ca4ef00b
commit 8519ec094f
8 changed files with 428 additions and 43 deletions

View File

@@ -544,3 +544,11 @@ body {
.animate-glow { .animate-glow {
animation: glow 2s ease-in-out infinite; animation: glow 2s ease-in-out infinite;
} }
/* Line clamp utilities */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

View File

@@ -8,6 +8,7 @@ import { DashboardStats } from '@/components/dashboard/DashboardStats';
import { QuickActions } from '@/components/dashboard/QuickActions'; import { QuickActions } from '@/components/dashboard/QuickActions';
import { RecentTasks } from '@/components/dashboard/RecentTasks'; import { RecentTasks } from '@/components/dashboard/RecentTasks';
import { ProductivityAnalytics } from '@/components/dashboard/ProductivityAnalytics'; import { ProductivityAnalytics } from '@/components/dashboard/ProductivityAnalytics';
import { WelcomeSection } from '@/components/dashboard/WelcomeSection';
import { ProductivityMetrics } from '@/services/analytics/analytics'; import { ProductivityMetrics } from '@/services/analytics/analytics';
import { DeadlineMetrics } from '@/services/analytics/deadline-analytics'; import { DeadlineMetrics } from '@/services/analytics/deadline-analytics';
import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts'; import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts';
@@ -55,6 +56,9 @@ function HomePageContent({ productivityMetrics, deadlineMetrics }: {
/> />
<main className="container mx-auto px-6 py-8"> <main className="container mx-auto px-6 py-8">
{/* Section de bienvenue */}
<WelcomeSection />
{/* Statistiques */} {/* Statistiques */}
<DashboardStats stats={stats} /> <DashboardStats stats={stats} />

View File

@@ -2,7 +2,7 @@
import { Task } from '@/lib/types'; import { Task } from '@/lib/types';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { TaskCard } from '@/components/ui'; import { RecentTaskTimeline } from '@/components/ui/RecentTaskTimeline';
import { useTasksContext } from '@/contexts/TasksContext'; import { useTasksContext } from '@/contexts/TasksContext';
import Link from 'next/link'; import Link from 'next/link';
@@ -39,47 +39,27 @@ export function RecentTasks({ tasks }: RecentTasksProps) {
<p className="text-sm">Créez votre première tâche pour commencer</p> <p className="text-sm">Créez votre première tâche pour commencer</p>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-1">
{recentTasks.map((task) => ( {recentTasks.map((task) => (
<div key={task.id} className="relative group"> <RecentTaskTimeline
<TaskCard key={task.id}
variant="detailed"
source={task.source || 'manual'}
title={task.title} title={task.title}
description={task.description} description={task.description}
status={task.status} status={task.status}
priority={task.priority as 'low' | 'medium' | 'high' | 'urgent'} priority={task.priority as 'low' | 'medium' | 'high'}
tags={task.tags || []} tags={task.tags || []}
dueDate={task.dueDate} dueDate={task.dueDate}
completedAt={task.completedAt} completedAt={task.completedAt}
updatedAt={task.updatedAt}
source={task.source || 'manual'}
jiraKey={task.jiraKey} jiraKey={task.jiraKey}
jiraProject={task.jiraProject}
jiraType={task.jiraType}
tfsPullRequestId={task.tfsPullRequestId} tfsPullRequestId={task.tfsPullRequestId}
tfsProject={task.tfsProject}
tfsRepository={task.tfsRepository}
todosCount={task.todosCount}
availableTags={availableTags} availableTags={availableTags}
fontSize="small" onClick={() => {
onTitleClick={() => {
// Navigation vers le kanban avec la tâche sélectionnée // Navigation vers le kanban avec la tâche sélectionnée
window.location.href = `/kanban?taskId=${task.id}`; window.location.href = `/kanban?taskId=${task.id}`;
}} }}
/> />
{/* Overlay avec lien vers le kanban */}
<Link
href={`/kanban?taskId=${task.id}`}
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-[var(--primary)]/5 rounded-lg flex items-center justify-center"
title="Ouvrir dans le Kanban"
>
<div className="bg-[var(--primary)]/20 backdrop-blur-sm rounded-full p-2 border border-[var(--primary)]/30">
<svg className="w-4 h-4 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</div>
</Link>
</div>
))} ))}
</div> </div>
)} )}

View File

@@ -0,0 +1,213 @@
'use client';
import { useSession } from 'next-auth/react';
import { useState, useEffect } from 'react';
const WELCOME_GREETINGS = [
"Bienvenue",
"Salut",
"Coucou",
"Hello",
"Hey",
"Salutations",
"Bonjour",
"Hola",
"Ciao",
"Yo",
];
const WELCOME_MESSAGES = [
"Prêt à conquérir la journée ? 🚀",
"Votre productivité vous attend ! ⚡",
"C'est parti pour une journée productive ! 💪",
"Organisons ensemble vos tâches ! 📋",
"Votre tableau de bord vous attend ! 🎯",
"Prêt à faire la différence ? ✨",
"Concentrons-nous sur l'essentiel ! 🎯",
"Une nouvelle journée, de nouvelles opportunités ! 🌟",
"Votre succès commence ici ! 🏆",
"Transformons vos objectifs en réalité ! 🎪",
"C'est l'heure de briller ! ⭐",
"Votre efficacité n'attend que vous ! 🔥",
"Organisons votre succès ! 📊",
"Prêt à dépasser vos limites ? 🚀",
"Votre productivité vous remercie ! 🙏",
"C'est parti pour une journée exceptionnelle ! 🌈",
"Votre organisation parfaite vous attend ! 🎨",
"Prêt à accomplir de grandes choses ? 🏅",
"Votre motivation est votre force ! 💎",
"Créons ensemble votre succès ! 🎭",
// Messages humoristiques
"Attention, productivité en approche ! 🚨",
"Votre cerveau va être bien occupé ! 🧠",
"Préparez-vous à être impressionné par vous-même ! 😎",
"Mode super-héros activé ! 🦸‍♂️",
"Votre liste de tâches tremble déjà ! 😱",
"Prêt à faire exploser vos objectifs ? 💥",
"Votre procrastination n'a qu'à bien se tenir ! 😤",
"C'est l'heure de montrer qui est le boss ! 👑",
"Votre café peut attendre, vos tâches non ! ☕",
"Prêt à devenir la légende de la productivité ? 🏆",
"Attention, efficacité maximale détectée ! ⚡",
"Votre motivation est plus forte que le café ! ☕",
"Prêt à faire rougir votre calendrier ? 📅",
"Votre énergie positive est contagieuse ! 😄",
"C'est parti pour une journée épique ! 🎬",
"Votre détermination brille plus que le soleil ! ☀️",
"Prêt à transformer le chaos en ordre ? 🎯",
"Votre focus est plus précis qu'un laser ! 🔴",
"C'est l'heure de montrer vos super-pouvoirs ! 🦸‍♀️",
"Votre organisation va faire des jaloux ! 😏",
"Prêt à devenir le héros de votre propre histoire ? 📚",
"Votre productivité va battre des records ! 🏃‍♂️",
"Attention, génie au travail ! 🧪",
"Votre créativité déborde ! 🎨",
"Prêt à faire trembler vos deadlines ? ⏰",
"Votre énergie positive illumine la pièce ! 💡",
"C'est parti pour une aventure productive ! 🗺️",
"Votre motivation est plus forte que la gravité ! 🌍",
"Prêt à devenir le maître de l'organisation ? 🎭",
"Votre efficacité va faire des étincelles ! ✨"
];
const TIME_BASED_MESSAGES = {
morning: [
"Bonjour ! Une belle journée vous attend ! ☀️",
"Réveillez-vous, c'est l'heure de briller ! 🌅",
"Le matin est le moment parfait pour commencer ! 🌄",
"Une nouvelle journée, de nouvelles possibilités ! 🌞",
"Bonjour ! Votre café vous attend ! ☕",
"Réveillez-vous, les tâches n'attendent pas ! ⏰",
"Bonjour ! Prêt à conquérir le monde ? 🌍",
"Le matin, tout est possible ! 🌅",
"Bonjour ! Votre motivation vous appelle ! 📞",
"Réveillez-vous, c'est l'heure de la productivité ! ⚡"
],
afternoon: [
"Bon après-midi ! Continuons sur cette lancée ! 🌤️",
"L'après-midi est parfait pour avancer ! ☀️",
"Encore quelques heures pour accomplir vos objectifs ! ⏰",
"L'énergie de l'après-midi vous porte ! 💪",
"Bon après-midi ! Le momentum continue ! 🚀",
"L'après-midi, c'est l'heure de l'efficacité ! ⚡",
"Bon après-midi ! Votre café de 14h vous attend ! ☕",
"L'après-midi, tout s'accélère ! 🏃‍♂️",
"Bon après-midi ! Prêt pour la deuxième mi-temps ? ⚽",
"L'après-midi, c'est l'heure de briller ! ✨"
],
evening: [
"Bonsoir ! Terminons la journée en beauté ! 🌆",
"Le soir est idéal pour finaliser vos tâches ! 🌇",
"Une dernière poussée avant la fin de journée ! 🌃",
"Le crépuscule vous accompagne ! 🌅",
"Bonsoir ! Prêt pour le sprint final ? 🏃‍♀️",
"Le soir, c'est l'heure de la victoire ! 🏆",
"Bonsoir ! Votre récompense vous attend ! 🎁",
"Le soir, on termine en héros ! 🦸‍♂️",
"Bonsoir ! Prêt à clôturer cette journée ? 📝",
"Le soir, c'est l'heure de la satisfaction ! 😌"
]
};
function getTimeBasedMessage(): string {
const hour = new Date().getHours();
if (hour >= 5 && hour < 12) {
return TIME_BASED_MESSAGES.morning[Math.floor(Math.random() * TIME_BASED_MESSAGES.morning.length)];
} else if (hour >= 12 && hour < 18) {
return TIME_BASED_MESSAGES.afternoon[Math.floor(Math.random() * TIME_BASED_MESSAGES.afternoon.length)];
} else {
return TIME_BASED_MESSAGES.evening[Math.floor(Math.random() * TIME_BASED_MESSAGES.evening.length)];
}
}
function getRandomWelcomeMessage(): string {
return WELCOME_MESSAGES[Math.floor(Math.random() * WELCOME_MESSAGES.length)];
}
function getRandomGreeting(): string {
return WELCOME_GREETINGS[Math.floor(Math.random() * WELCOME_GREETINGS.length)];
}
export function WelcomeSection() {
const { data: session } = useSession();
const [welcomeMessage, setWelcomeMessage] = useState<string>('');
const [timeMessage, setTimeMessage] = useState<string>('');
const [greeting, setGreeting] = useState<string>('');
useEffect(() => {
// Générer un message de bienvenue aléatoire
setWelcomeMessage(getRandomWelcomeMessage());
setTimeMessage(getTimeBasedMessage());
setGreeting(getRandomGreeting());
}, []);
if (!session?.user) {
return null;
}
const displayName = session.user.name ||
`${session.user.firstName || ''} ${session.user.lastName || ''}`.trim() ||
session.user.email;
return (
<div className="bg-gradient-to-r from-[var(--primary)]/10 via-[var(--accent)]/10 to-[var(--purple)]/10 rounded-xl p-6 mb-8 border border-[var(--primary)]/20">
<div className="flex flex-col md:flex-row items-center md:items-start gap-4">
{/* Avatar */}
<div className="flex-shrink-0">
{session.user.avatar ? (
<div className="relative">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={session.user.avatar}
alt="Avatar"
className="w-16 h-16 rounded-full object-cover border-3 border-[var(--primary)]/30 shadow-lg"
/>
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-[var(--success)] rounded-full border-3 border-[var(--card)] flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
) : (
<div className="w-16 h-16 rounded-full bg-[var(--primary)]/20 border-3 border-[var(--primary)]/30 flex items-center justify-center">
<svg className="w-8 h-8 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
)}
</div>
{/* Message de bienvenue */}
<div className="flex-1 text-center md:text-left">
<h1 className="text-2xl md:text-3xl font-mono font-bold text-[var(--foreground)] mb-2">
{greeting}, {displayName} !
</h1>
<p className="text-lg text-[var(--muted-foreground)] mb-2">
{timeMessage}
</p>
<p className="text-base text-[var(--primary)] font-medium">
{welcomeMessage}
</p>
</div>
{/* Bouton pour changer le message */}
<div className="flex-shrink-0">
<button
onClick={() => {
setWelcomeMessage(getRandomWelcomeMessage());
setTimeMessage(getTimeBasedMessage());
setGreeting(getRandomGreeting());
}}
className="p-2 rounded-lg bg-[var(--card)] border border-[var(--border)] hover:bg-[var(--card-hover)] transition-colors"
title="Changer le message"
>
<svg className="w-5 h-5 text-[var(--muted-foreground)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
</div>
);
}

View File

@@ -9,6 +9,7 @@ import { MetricCard } from '@/components/ui/MetricCard';
import { AchievementCard } from '@/components/ui/AchievementCard'; import { AchievementCard } from '@/components/ui/AchievementCard';
import { ChallengeCard } from '@/components/ui/ChallengeCard'; import { ChallengeCard } from '@/components/ui/ChallengeCard';
import { SkeletonCard } from '@/components/ui/SkeletonCard'; import { SkeletonCard } from '@/components/ui/SkeletonCard';
import { RecentTaskTimeline } from '@/components/ui/RecentTaskTimeline';
import { AchievementData } from '@/components/ui/AchievementCard'; import { AchievementData } from '@/components/ui/AchievementCard';
import { ChallengeData } from '@/components/ui/ChallengeCard'; import { ChallengeData } from '@/components/ui/ChallengeCard';
@@ -224,6 +225,54 @@ export function CardsSection() {
/> />
</div> </div>
</div> </div>
{/* Recent Task Timeline */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">Recent Task Timeline</h3>
<div className="space-y-1">
<RecentTaskTimeline
title="Implement user authentication"
description="Add login and registration functionality with JWT tokens"
status="in_progress"
priority="high"
tags={['auth', 'security', 'backend']}
dueDate={new Date(Date.now() + 3 * 86400000)}
source="jira"
jiraKey="PROJ-123"
updatedAt={new Date(Date.now() - 2 * 3600000)}
/>
<RecentTaskTimeline
title="Design new dashboard"
description="Create a modern dashboard interface with analytics"
status="todo"
priority="medium"
tags={['design', 'ui', 'frontend']}
dueDate={new Date(Date.now() + 7 * 86400000)}
source="manual"
updatedAt={new Date(Date.now() - 1 * 86400000)}
/>
<RecentTaskTimeline
title="Fix critical bug in payment system"
description="Resolve issue with payment processing"
status="done"
priority="high"
tags={['bug', 'payment', 'critical']}
completedAt={new Date(Date.now() - 1 * 3600000)}
source="tfs"
tfsPullRequestId={456}
/>
<RecentTaskTimeline
title="Update documentation"
description="Update API documentation for new endpoints"
status="in_progress"
priority="low"
tags={['docs', 'api']}
source="reminders"
updatedAt={new Date(Date.now() - 30 * 60000)}
/>
</div>
</div>
{/* Skeleton Cards */} {/* Skeleton Cards */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">Skeleton Cards</h3> <h3 className="text-lg font-medium text-[var(--foreground)]">Skeleton Cards</h3>

View File

@@ -66,9 +66,10 @@ export function FeedbackSection() {
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">Alert Banner</h3> <h3 className="text-lg font-medium text-[var(--foreground)]">Alert Banner</h3>
<AlertBanner <AlertBanner
title="Alertes importantes"
items={alertItems} items={alertItems}
onDismiss={(id) => console.log('Dismiss alert:', id)} variant="warning"
onAction={(id, action) => console.log('Alert action:', id, action)} onItemClick={(item) => console.log('Alert clicked:', item)}
/> />
</div> </div>

View File

@@ -0,0 +1,129 @@
import { HTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
import { StatusBadge } from './StatusBadge';
import { TaskStatus } from '@/lib/types';
import { PriorityBadge } from './PriorityBadge';
import { TagDisplay } from './TagDisplay';
import { formatDateForDisplay } from '@/lib/date-utils';
interface RecentTaskTimelineProps extends HTMLAttributes<HTMLDivElement> {
title: string;
description?: string;
tags?: string[];
priority?: 'low' | 'medium' | 'high';
status?: TaskStatus;
dueDate?: Date;
completedAt?: Date;
updatedAt?: Date;
source?: 'manual' | 'jira' | 'tfs' | 'reminders';
jiraKey?: string;
tfsPullRequestId?: number;
onClick?: () => void;
className?: string;
availableTags?: Array<{ id: string; name: string; color: string }>;
}
export function RecentTaskTimeline({
title,
description,
tags = [],
priority,
status = 'todo',
dueDate,
completedAt,
updatedAt,
source = 'manual',
jiraKey,
tfsPullRequestId,
onClick,
className,
availableTags = [],
...props
}: RecentTaskTimelineProps) {
const getSourceIcon = () => {
switch (source) {
case 'jira':
return <div className="w-2 h-2 bg-[var(--primary)] rounded-full" />;
case 'tfs':
return <div className="w-2 h-2 bg-[var(--blue)] rounded-full" />;
case 'reminders':
return <div className="w-2 h-2 bg-[var(--accent)] rounded-full" />;
default:
return <div className="w-2 h-2 bg-[var(--gray)] rounded-full" />;
}
};
const getTimeInfo = () => {
if (completedAt) {
return `Terminé ${formatDateForDisplay(completedAt)}`;
}
if (dueDate) {
return `Échéance ${formatDateForDisplay(dueDate)}`;
}
if (updatedAt) {
return `Modifié ${formatDateForDisplay(updatedAt)}`;
}
return null;
};
return (
<div
className={cn(
"group relative flex items-start gap-4 p-3 rounded-lg hover:bg-[var(--card-hover)] transition-colors cursor-pointer",
className
)}
onClick={onClick}
{...props}
>
{/* Timeline dot */}
<div className="flex-shrink-0 mt-2">
{getSourceIcon()}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<h4 className="font-medium text-[var(--foreground)] text-sm group-hover:text-[var(--primary)] transition-colors">
{title}
</h4>
<div className="flex items-center gap-1 flex-shrink-0">
{priority && <PriorityBadge priority={priority} />}
<StatusBadge status={status} />
</div>
</div>
{description && (
<p className="text-xs text-[var(--muted-foreground)] mb-2 line-clamp-1">
{description}
</p>
)}
{tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={tags}
availableTags={availableTags}
maxTags={2}
size="sm"
/>
</div>
)}
<div className="flex items-center justify-between text-xs text-[var(--muted-foreground)]">
<div className="flex items-center gap-2">
{jiraKey && <span className="font-mono text-[var(--primary)]">{jiraKey}</span>}
{tfsPullRequestId && <span className="font-mono text-[var(--blue)]">PR #{tfsPullRequestId}</span>}
</div>
<span>{getTimeInfo()}</span>
</div>
</div>
{/* Arrow indicator */}
<div className="flex-shrink-0 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
<svg className="w-4 h-4 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
);
}

View File

@@ -10,6 +10,7 @@ export { StatCard } from './StatCard';
export { ProgressBar } from './ProgressBar'; export { ProgressBar } from './ProgressBar';
export { ActionCard } from './ActionCard'; export { ActionCard } from './ActionCard';
export { TaskCard } from './TaskCard'; export { TaskCard } from './TaskCard';
export { RecentTaskTimeline } from './RecentTaskTimeline';
export { MetricCard } from './MetricCard'; export { MetricCard } from './MetricCard';
// Composants Kanban // Composants Kanban