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:
@@ -544,3 +544,11 @@ body {
|
||||
.animate-glow {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DashboardStats } from '@/components/dashboard/DashboardStats';
|
||||
import { QuickActions } from '@/components/dashboard/QuickActions';
|
||||
import { RecentTasks } from '@/components/dashboard/RecentTasks';
|
||||
import { ProductivityAnalytics } from '@/components/dashboard/ProductivityAnalytics';
|
||||
import { WelcomeSection } from '@/components/dashboard/WelcomeSection';
|
||||
import { ProductivityMetrics } from '@/services/analytics/analytics';
|
||||
import { DeadlineMetrics } from '@/services/analytics/deadline-analytics';
|
||||
import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts';
|
||||
@@ -55,6 +56,9 @@ function HomePageContent({ productivityMetrics, deadlineMetrics }: {
|
||||
/>
|
||||
|
||||
<main className="container mx-auto px-6 py-8">
|
||||
{/* Section de bienvenue */}
|
||||
<WelcomeSection />
|
||||
|
||||
{/* Statistiques */}
|
||||
<DashboardStats stats={stats} />
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Task } from '@/lib/types';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { TaskCard } from '@/components/ui';
|
||||
import { RecentTaskTimeline } from '@/components/ui/RecentTaskTimeline';
|
||||
import { useTasksContext } from '@/contexts/TasksContext';
|
||||
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>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
{recentTasks.map((task) => (
|
||||
<div key={task.id} className="relative group">
|
||||
<TaskCard
|
||||
variant="detailed"
|
||||
source={task.source || 'manual'}
|
||||
title={task.title}
|
||||
description={task.description}
|
||||
status={task.status}
|
||||
priority={task.priority as 'low' | 'medium' | 'high' | 'urgent'}
|
||||
tags={task.tags || []}
|
||||
dueDate={task.dueDate}
|
||||
completedAt={task.completedAt}
|
||||
jiraKey={task.jiraKey}
|
||||
jiraProject={task.jiraProject}
|
||||
jiraType={task.jiraType}
|
||||
tfsPullRequestId={task.tfsPullRequestId}
|
||||
tfsProject={task.tfsProject}
|
||||
tfsRepository={task.tfsRepository}
|
||||
todosCount={task.todosCount}
|
||||
availableTags={availableTags}
|
||||
fontSize="small"
|
||||
onTitleClick={() => {
|
||||
// Navigation vers le kanban avec la tâche sélectionnée
|
||||
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>
|
||||
<RecentTaskTimeline
|
||||
key={task.id}
|
||||
title={task.title}
|
||||
description={task.description}
|
||||
status={task.status}
|
||||
priority={task.priority as 'low' | 'medium' | 'high'}
|
||||
tags={task.tags || []}
|
||||
dueDate={task.dueDate}
|
||||
completedAt={task.completedAt}
|
||||
updatedAt={task.updatedAt}
|
||||
source={task.source || 'manual'}
|
||||
jiraKey={task.jiraKey}
|
||||
tfsPullRequestId={task.tfsPullRequestId}
|
||||
availableTags={availableTags}
|
||||
onClick={() => {
|
||||
// Navigation vers le kanban avec la tâche sélectionnée
|
||||
window.location.href = `/kanban?taskId=${task.id}`;
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
213
src/components/dashboard/WelcomeSection.tsx
Normal file
213
src/components/dashboard/WelcomeSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { MetricCard } from '@/components/ui/MetricCard';
|
||||
import { AchievementCard } from '@/components/ui/AchievementCard';
|
||||
import { ChallengeCard } from '@/components/ui/ChallengeCard';
|
||||
import { SkeletonCard } from '@/components/ui/SkeletonCard';
|
||||
import { RecentTaskTimeline } from '@/components/ui/RecentTaskTimeline';
|
||||
import { AchievementData } from '@/components/ui/AchievementCard';
|
||||
import { ChallengeData } from '@/components/ui/ChallengeCard';
|
||||
|
||||
@@ -224,6 +225,54 @@ export function CardsSection() {
|
||||
/>
|
||||
</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 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-[var(--foreground)]">Skeleton Cards</h3>
|
||||
|
||||
@@ -66,9 +66,10 @@ export function FeedbackSection() {
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-[var(--foreground)]">Alert Banner</h3>
|
||||
<AlertBanner
|
||||
title="Alertes importantes"
|
||||
items={alertItems}
|
||||
onDismiss={(id) => console.log('Dismiss alert:', id)}
|
||||
onAction={(id, action) => console.log('Alert action:', id, action)}
|
||||
variant="warning"
|
||||
onItemClick={(item) => console.log('Alert clicked:', item)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
129
src/components/ui/RecentTaskTimeline.tsx
Normal file
129
src/components/ui/RecentTaskTimeline.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export { StatCard } from './StatCard';
|
||||
export { ProgressBar } from './ProgressBar';
|
||||
export { ActionCard } from './ActionCard';
|
||||
export { TaskCard } from './TaskCard';
|
||||
export { RecentTaskTimeline } from './RecentTaskTimeline';
|
||||
export { MetricCard } from './MetricCard';
|
||||
|
||||
// Composants Kanban
|
||||
|
||||
Reference in New Issue
Block a user