Files
towercontrol/src/components/ui/Header.tsx
Julien Froidefond eac9e9a0bb feat: update TODO.md and enhance dashboard components
- Marked several UI/UX tasks as complete in TODO.md, including improvements for Kanban icons, tag visibility, recent tasks display, and header responsiveness.
- Updated PriorityDistributionChart to adjust height for better layout.
- Refined IntegrationFilter to improve filter display and added new trigger class for dropdowns.
- Replaced RecentTaskTimeline with TaskCard in RecentTasks for better consistency.
- Enhanced TagDistributionChart with improved tooltip and legend styling.
- Updated DesktopControls and MobileControls to use lucide-react icons for filters and search functionality.
- Removed RecentTaskTimeline component for cleaner codebase.
2025-10-04 07:17:39 +02:00

392 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useTheme } from '@/contexts/ThemeContext';
import { useJiraConfig } from '@/contexts/JiraConfigContext';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { useState } from 'react';
import { Theme, THEME_CONFIG, getThemeIcon } from '@/lib/ui-config';
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
import { AuthButton } from '@/components/AuthButton';
import { useSession, signOut } from 'next-auth/react';
import { Check, X, Menu } from 'lucide-react';
interface HeaderProps {
title?: string;
subtitle?: string;
syncing?: boolean;
}
export function Header({ title = "TowerControl", subtitle = "Task Management", syncing = false }: HeaderProps) {
const { theme, setTheme } = useTheme();
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [tabletMenuOpen, setTabletMenuOpen] = useState(false);
const [themeDropdownOpen, setThemeDropdownOpen] = useState(false);
const { openModal: openShortcutsModal } = useKeyboardShortcutsModal();
const { data: session } = useSession();
// Liste des thèmes disponibles avec leurs labels et icônes
const themes: { value: Theme; label: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeValue => {
return {
value: themeValue,
label: themeValue.charAt(0).toUpperCase() + themeValue.slice(1).replace('_', ' '),
icon: getThemeIcon(themeValue)
};
});
// Fonction pour déterminer si un lien est actif
const isActiveLink = (href: string) => {
if (href === '/') {
return pathname === '/';
}
return pathname.startsWith(href);
};
// Fonction pour obtenir les classes CSS d'un lien (desktop)
const getLinkClasses = (href: string) => {
const baseClasses = "font-mono text-sm uppercase tracking-wider transition-colors px-3 py-1.5 rounded-md";
if (isActiveLink(href)) {
return `${baseClasses} text-[var(--primary)] bg-[var(--primary)]/10 border border-[var(--primary)]/30`;
}
return `${baseClasses} text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]`;
};
// Fonction pour obtenir les classes CSS d'un lien (mobile)
const getMobileLinkClasses = (href: string) => {
const baseClasses = "font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left";
if (isActiveLink(href)) {
return `${baseClasses} text-[var(--primary)] bg-[var(--primary)]/10 border border-[var(--primary)]/30`;
}
return `${baseClasses} text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]`;
};
// Liste des liens de navigation
const navLinks = [
{ href: '/', label: 'Dashboard' },
{ href: '/kanban', label: 'Kanban' },
{ href: '/daily', label: 'Daily' },
{ href: '/weekly-manager', label: 'Weekly' },
...(isJiraConfigured ? [{ href: '/jira-dashboard', label: `Jira${jiraConfig?.projectKey ? ` (${jiraConfig.projectKey})` : ''}` }] : []),
{ href: '/settings', label: 'Settings' }
];
return (
<header className="relative z-50 bg-[var(--card)]/80 border-b border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20">
<div className="container mx-auto px-4 sm:px-6 py-4">
{/* Layout mobile/tablette */}
<div className="lg:hidden">
<div className="flex items-center justify-between">
{/* Titre et status */}
<div className="flex items-center gap-3 sm:gap-4 min-w-0 flex-1">
<div className={`w-3 h-3 rounded-full shadow-lg flex-shrink-0 ${
syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div>
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider truncate">
{title}
</h1>
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-xs sm:text-sm truncate">
{subtitle}
</p>
</div>
</div>
{/* Controls mobile/tablette */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* Keyboard Shortcuts */}
<button
onClick={openShortcutsModal}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
title="Raccourcis clavier (Cmd+?)"
>
</button>
{/* Theme Dropdown */}
<div className="relative">
<button
onClick={() => setThemeDropdownOpen(!themeDropdownOpen)}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
title="Select theme"
>
{themes.find(t => t.value === theme)?.icon || '🎨'}
</button>
{themeDropdownOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[200]"
onClick={() => setThemeDropdownOpen(false)}
/>
{/* Dropdown */}
<div className="absolute right-0 top-full mt-2 w-48 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] overflow-hidden">
<div className="py-2">
{themes.map((themeOption) => (
<button
key={themeOption.value}
onClick={() => {
setTheme(themeOption.value);
setThemeDropdownOpen(false);
}}
className={`w-full text-left px-4 py-2 text-sm transition-colors flex items-center gap-3 ${
theme === themeOption.value
? 'text-[var(--primary)] bg-[var(--primary)]/10'
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--card-hover)]'
}`}
>
<span className="text-base">{themeOption.icon}</span>
<span className="font-mono">{themeOption.label}</span>
{theme === themeOption.value && (
<Check className="w-4 h-4 ml-auto" />
)}
</button>
))}
</div>
</div>
</>
)}
</div>
{/* Menu burger */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
title="Toggle menu"
>
{mobileMenuOpen ? (
<X className="w-5 h-5" />
) : (
<Menu className="w-5 h-5" />
)}
</button>
</div>
</div>
{/* Auth controls à droite mobile - dans la ligne principale */}
<div className="hidden sm:block">
<AuthButton />
</div>
</div>
{/* Ligne Auth séparée sur très petits écrans */}
<div className="lg:hidden sm:hidden flex justify-end pt-2">
<AuthButton />
</div>
{/* Layout desktop - une seule ligne comme avant */}
<div className="hidden lg:flex items-center justify-between gap-6">
{/* Titre et status */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-4 min-w-0">
<div className={`w-3 h-3 rounded-full shadow-lg ${
syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div>
<div className="min-w-0">
<h1 className="text-xl xl:text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider truncate">
<span className="sm:hidden">{title}</span>
<span className="hidden sm:inline">{title}</span>
</h1>
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-xs sm:text-sm truncate">
{subtitle}
</p>
</div>
</div>
{/* Navigation desktop */}
<nav className="flex items-center gap-1 xl:gap-2 flex-wrap">
{navLinks.slice(0, 4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={`${getLinkClasses(href)} text-xs xl:text-sm`}
>
{label}
</Link>
))}
{/* Plus d'éléments sur très grands écrans */}
<div className="hidden 2xl:flex items-center gap-1">
{navLinks.slice(4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={getLinkClasses(href)}
>
{label}
</Link>
))}
</div>
{/* Menu dropdown pour écrans moyens */}
<div className="xl:hidden relative">
<button
onClick={() => setTabletMenuOpen(!tabletMenuOpen)}
className="font-mono text-xs uppercase tracking-wider transition-colors px-2 py-1.5 rounded-md text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]"
title="Plus de liens"
>
</button>
{tabletMenuOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[200]"
onClick={() => setTabletMenuOpen(false)}
/>
{/* Menu items */}
<div className="absolute right-0 top-full mt-2 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] py-2 min-w-[119px]">
{navLinks.slice(4).map(({ href, label }) => (
<Link
key={href}
href={href}
className={`block px-4 py-2 text-sm transition-colors ${getMobileLinkClasses(href)}`}
onClick={() => setTabletMenuOpen(false)}
>
{label}
</Link>
))}
</div>
</>
)}
</div>
{/* Keyboard Shortcuts desktop */}
<button
onClick={openShortcutsModal}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--card-hover)]"
title="Raccourcis clavier (Cmd+?)"
>
</button>
{/* Theme Dropdown desktop */}
<div className="relative">
<button
onClick={() => setThemeDropdownOpen(!themeDropdownOpen)}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--card-hover)]"
title="Select theme"
>
{themes.find(t => t.value === theme)?.icon || '🎨'}
</button>
{themeDropdownOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[200]"
onClick={() => setThemeDropdownOpen(false)}
/>
{/* Dropdown */}
<div className="absolute right-0 top-full mt-2 w-48 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[201] overflow-hidden">
<div className="py-2">
{themes.map((themeOption) => (
<button
key={themeOption.value}
onClick={() => {
setTheme(themeOption.value);
setThemeDropdownOpen(false);
}}
className={`w-full text-left px-4 py-2 text-sm transition-colors flex items-center gap-3 ${
theme === themeOption.value
? 'text-[var(--primary)] bg-[var(--primary)]/10'
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--card-hover)]'
}`}
>
<span className="text-base">{themeOption.icon}</span>
<span className="font-mono">{themeOption.label}</span>
{theme === themeOption.value && (
<Check className="w-4 h-4 ml-auto" />
)}
</button>
))}
</div>
</div>
</>
)}
</div>
</nav>
</div>
{/* Contrôles à droite */}
<div className="flex items-center gap-2">
<AuthButton />
</div>
</div>
</div>
{/* Menu mobile/tablette en modal */}
{mobileMenuOpen && (
<div className="lg:hidden fixed inset-0 z-[9999] flex items-start justify-center pt-20">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50"
onClick={() => setMobileMenuOpen(false)}
/>
{/* Modal content */}
<div className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl mx-4 w-full max-w-sm">
<nav className="p-6">
<div className="space-y-3">
{navLinks.map(({ href, label }) => (
<Link
key={href}
href={href}
className={getMobileLinkClasses(href)}
onClick={() => setMobileMenuOpen(false)}
>
{label}
</Link>
))}
{/* Séparateur */}
{session && (
<>
<div className="border-t border-[var(--border)]/30 my-4"></div>
{/* Lien profil */}
<Link
href="/profile"
className={getMobileLinkClasses('/profile')}
onClick={() => setMobileMenuOpen(false)}
>
Profil
</Link>
{/* Bouton déconnexion */}
<button
onClick={() => {
signOut({ callbackUrl: '/login' });
setMobileMenuOpen(false);
}}
className="font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left text-[var(--destructive)] hover:text-[var(--destructive)] hover:bg-[var(--destructive)]/10"
>
Déconnexion
</button>
</>
)}
</div>
</nav>
</div>
</div>
)}
</header>
);
}