feat: update TODO.md and refactor Header component
- Removed redundant theme handling code from Header component, improving readability and maintainability. - Integrated HeaderMobile and HeaderDesktop components for better responsive design. - Marked the task for repositioning the theme icon in the header as complete in TODO.md.
This commit is contained in:
5
TODO.md
5
TODO.md
@@ -21,17 +21,16 @@
|
||||
- [x] **EditModal task couleur calendrier** - Problème de couleur en ajout de taches dans tous les icones calendriers; colmler au thème
|
||||
- [x] **Weekly deux boutons actualiser** - Corriger la duplication des boutons d'actualisation
|
||||
- [x] **Solarized ne doit pas être un soleil** - Corriger l'icône du thème Solarized
|
||||
- [ ] **Settings : tag icônes actions** - Icônes trop petites dans les actions des tags
|
||||
- [ ] **Emoji interdit dans UI** - Vérifier et supprimer toutes les emojis dans l'interface, remplacer par lucide-react
|
||||
- [ ] **Settings intégration : icônes** - Problème avec les icônes "Arrêté" et "Actif" : doivent etre les memes
|
||||
- [ ] **Settings backup UI** - Revoir l'UI pour coller au style des intégrations
|
||||
- [ ] **Emoji interdit dans UI** - Vérifier et supprimer toutes les emojis dans l'interface, remplacer par lib d'icone
|
||||
- [ ] **AlertBanner : hover et bug** - Corriger les problèmes de hover et bugs
|
||||
- [ ] **Deux modales** - Problème de duplication de modales
|
||||
- [ ] **Control panel et select** - Problème avec les contrôles et sélecteurs
|
||||
- [ ] **TaskCard et Kanban transparence** - Appliquer la transparence sur le background et non sur la card
|
||||
- [X] **Recherche Kanban desktop controls** - Ajouter icône et label : "rechercher" pour rapetir
|
||||
- [ ] **Largeur page Kanban** - Réduire légèrement la largeur et revoir toutes les autres pages
|
||||
- [ ] **Icône thème à gauche du profil** - Repositionner l'icône de thème dans le header
|
||||
- [x] **Icône thème à gauche du profil** - Repositionner l'icône de thème dans le header
|
||||
- [ ] **Déconnexion trop petit et couleur** - Améliorer le bouton de déconnexion
|
||||
- [ ] **Fond modal trop opaque** - Réduire l'opacité du fond des modales
|
||||
- [ ] **Couleurs thème clair et TFS Jira Kanban** - Harmoniser les couleurs du thème clair
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
'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';
|
||||
import { HeaderMobile } from './header/HeaderMobile';
|
||||
import { HeaderDesktop } from './header/HeaderDesktop';
|
||||
|
||||
interface HeaderProps {
|
||||
title?: string;
|
||||
@@ -18,373 +10,12 @@ interface HeaderProps {
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<HeaderMobile title={title} subtitle={subtitle} syncing={syncing} />
|
||||
<HeaderDesktop title={title} subtitle={subtitle} syncing={syncing} />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
59
src/components/ui/header/HeaderControls.tsx
Normal file
59
src/components/ui/header/HeaderControls.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ThemeDropdown } from './ThemeDropdown';
|
||||
import { HeaderNavigation } from './HeaderNavigation';
|
||||
|
||||
interface HeaderControlsProps {
|
||||
variant: 'desktop' | 'mobile';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HeaderControls({ variant, className = '' }: HeaderControlsProps) {
|
||||
const [tabletMenuOpen, setTabletMenuOpen] = useState(false);
|
||||
|
||||
if (variant === 'mobile') {
|
||||
return (
|
||||
<div className={`flex items-center gap-2 flex-shrink-0 ${className}`}>
|
||||
{/* Theme Dropdown */}
|
||||
<ThemeDropdown variant="mobile" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop version
|
||||
return (
|
||||
<div className={`flex items-center gap-2 ${className}`}>
|
||||
{/* Navigation */}
|
||||
<HeaderNavigation variant="desktop" />
|
||||
|
||||
{/* Plus de navigation 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]">
|
||||
<HeaderNavigation
|
||||
variant="mobile"
|
||||
onLinkClick={() => setTabletMenuOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
src/components/ui/header/HeaderDesktop.tsx
Normal file
57
src/components/ui/header/HeaderDesktop.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { AuthButton } from '@/components/AuthButton';
|
||||
import { HeaderControls } from './HeaderControls';
|
||||
import { ThemeDropdown } from './ThemeDropdown';
|
||||
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
|
||||
|
||||
interface HeaderDesktopProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
syncing: boolean;
|
||||
}
|
||||
|
||||
export function HeaderDesktop({ title, subtitle, syncing }: HeaderDesktopProps) {
|
||||
const { openModal: openShortcutsModal } = useKeyboardShortcutsModal();
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<HeaderControls variant="desktop" />
|
||||
</div>
|
||||
|
||||
{/* Contrôles à droite */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 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>
|
||||
<ThemeDropdown variant="desktop" />
|
||||
<AuthButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
116
src/components/ui/header/HeaderMobile.tsx
Normal file
116
src/components/ui/header/HeaderMobile.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { X, Menu } from 'lucide-react';
|
||||
import { signOut } from 'next-auth/react';
|
||||
import Link from 'next/link';
|
||||
import { HeaderControls } from './HeaderControls';
|
||||
import { HeaderNavigation } from './HeaderNavigation';
|
||||
import { AuthButton } from '@/components/AuthButton';
|
||||
|
||||
interface HeaderMobileProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
syncing: boolean;
|
||||
}
|
||||
|
||||
export function HeaderMobile({ title, subtitle, syncing }: HeaderMobileProps) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
// 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";
|
||||
|
||||
// Simplifier pour éviter la duplication de logique
|
||||
return `${baseClasses} text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 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 */}
|
||||
<HeaderControls variant="mobile" />
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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">
|
||||
<HeaderNavigation
|
||||
variant="mobile"
|
||||
onLinkClick={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Séparateur */}
|
||||
<div className="border-t border-[var(--border)]/30 my-4"></div>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
101
src/components/ui/header/HeaderNavigation.tsx
Normal file
101
src/components/ui/header/HeaderNavigation.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useJiraConfig } from '@/contexts/JiraConfigContext';
|
||||
|
||||
interface HeaderNavigationProps {
|
||||
variant: 'desktop' | 'mobile';
|
||||
className?: string;
|
||||
onLinkClick?: () => void;
|
||||
}
|
||||
|
||||
export function HeaderNavigation({ variant, className = '', onLinkClick }: HeaderNavigationProps) {
|
||||
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
|
||||
const pathname = usePathname();
|
||||
|
||||
// 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' }
|
||||
];
|
||||
|
||||
// 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"]`;
|
||||
};
|
||||
|
||||
if (variant === 'mobile') {
|
||||
return (
|
||||
<nav className={`space-y-3 ${className}`}>
|
||||
{navLinks.map(({ href, label }) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className={getMobileLinkClasses(href)}
|
||||
onClick={onLinkClick}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop version
|
||||
return (
|
||||
<nav className={`flex items-center gap-1 xl:gap-2 flex-wrap ${className}`}>
|
||||
{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>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
74
src/components/ui/header/ThemeDropdown.tsx
Normal file
74
src/components/ui/header/ThemeDropdown.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Check } from 'lucide-react';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { Theme, THEME_CONFIG, getThemeIcon } from '@/lib/ui-config';
|
||||
|
||||
interface ThemeDropdownProps {
|
||||
variant: 'desktop' | 'mobile';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ThemeDropdown({ variant, className = '' }: ThemeDropdownProps) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [themeDropdownOpen, setThemeDropdownOpen] = useState(false);
|
||||
|
||||
// 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)
|
||||
};
|
||||
});
|
||||
|
||||
const buttonSize = variant === 'mobile' ? 'p-2' : 'p-1';
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
<button
|
||||
onClick={() => setThemeDropdownOpen(!themeDropdownOpen)}
|
||||
className={`text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors ${buttonSize} 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user