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 {
|
.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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
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 { 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
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 { 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
|
||||||
|
|||||||
Reference in New Issue
Block a user