feat: enhance mobile and desktop layouts in Daily and Kanban pages

- Refactored `DailyPageClient` to prioritize mobile layout with today's section first and calendar at the bottom for better usability.
- Updated `KanbanPageClient` to include responsive controls for mobile, improving task management experience.
- Adjusted `DailyCheckboxItem` and `DailySection` for better touch targets and responsive design.
- Cleaned up `TODO.md` to reflect changes in mobile interface considerations and task management features.
This commit is contained in:
Julien Froidefond
2025-09-21 21:37:30 +02:00
parent 2194744eef
commit 361fc0eaac
9 changed files with 435 additions and 217 deletions

37
TODO.md
View File

@@ -124,43 +124,6 @@
- **Performance** : Index sur `userId`, pagination pour gros volumes - **Performance** : Index sur `userId`, pagination pour gros volumes
- **Migration** : Script de migration des données existantes - **Migration** : Script de migration des données existantes
### 📱 Interface mobile adaptée (PROJET MAJEUR)
#### **Problème actuel**
- Kanban non adapté aux écrans tactiles petits
- Drag & drop difficile sur mobile
- Interface desktop-first
#### **Solution : Interface mobile dédiée**
- [ ] **Phase 1: Détection et responsive**
- [ ] Détection mobile/desktop (useMediaQuery)
- [ ] Composant de switch automatique d'interface
- [ ] Breakpoints adaptés pour tablettes
- [ ] **Phase 2: Interface mobile pour les tâches**
- [ ] **Vue liste simple** : Kanban simple OK, mais swimlane KO. Ajouter une autre interface plus simple pour mobile en plus du Kanban Simple
- [ ] Liste verticale avec statuts en badges
- [ ] Actions par swipe (marquer terminé, changer statut)
- [ ] Filtres simplifiés (dropdown au lieu de sidebar)
- [ ] **Actions tactiles**
- [ ] Tap pour voir détails
- [ ] Long press pour menu contextuel
- [ ] Swipe left/right pour actions rapides
- [ ] **Navigation mobile**
- [ ] Bottom navigation bar
- [ ] Sections : Tâches, Daily, Jira, Profil
- [ ] **Phase 3: Daily mobile optimisé**
- [ ] Checkboxes plus grandes (touch-friendly)
- [ ] Ajout rapide par bouton flottant
- [ ] On ne voit que aujourd'hui et au swipe on va en avant en arrière par jour
#### **Considérations UX mobile**
- **Simplicité** : Moins d'options visibles, plus de navigation
- **Tactile** : Boutons plus grands, zones de touch optimisées
- **Performance** : Lazy loading, virtualisation pour longues listes
- **Offline** : Cache local pour usage sans réseau (PWA)
--- ---
*Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer vers une plateforme d'intégration complète.* *Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer vers une plateforme d'intégration complète.*

View File

@@ -212,52 +212,84 @@ export function DailyPageClient({
{/* Contenu principal */} {/* Contenu principal */}
<main className="container mx-auto px-4 py-8"> <main className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6"> {/* Layout Mobile uniquement - Section Aujourd'hui en premier */}
{/* Calendrier - toujours visible */} <div className="block sm:hidden">
<div className="xl:col-span-1">
<DailyCalendar
currentDate={currentDate}
onDateSelect={handleDateSelect}
dailyDates={dailyDates}
/>
</div>
{/* Sections daily */}
{dailyView && ( {dailyView && (
<div className="xl:col-span-2 grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="space-y-6">
{/* Section Hier */} {/* Section Aujourd'hui - Mobile First */}
<DailySection <DailySection
title={getYesterdayTitle()} title={getTodayTitle()}
date={getYesterdayDate()} date={getTodayDate()}
checkboxes={dailyView.yesterday} checkboxes={dailyView.today}
onAddCheckbox={handleAddYesterdayCheckbox} onAddCheckbox={handleAddTodayCheckbox}
onToggleCheckbox={handleToggleCheckbox} onToggleCheckbox={handleToggleCheckbox}
onUpdateCheckbox={handleUpdateCheckbox} onUpdateCheckbox={handleUpdateCheckbox}
onDeleteCheckbox={handleDeleteCheckbox} onDeleteCheckbox={handleDeleteCheckbox}
onReorderCheckboxes={handleReorderCheckboxes} onReorderCheckboxes={handleReorderCheckboxes}
onToggleAll={toggleAllYesterday} onToggleAll={toggleAllToday}
saving={saving} saving={saving}
refreshing={refreshing} refreshing={refreshing}
/> />
{/* Section Aujourd'hui */} {/* Calendrier en bas sur mobile */}
<DailySection <DailyCalendar
title={getTodayTitle()} currentDate={currentDate}
date={getTodayDate()} onDateSelect={handleDateSelect}
checkboxes={dailyView.today} dailyDates={dailyDates}
onAddCheckbox={handleAddTodayCheckbox} />
onToggleCheckbox={handleToggleCheckbox}
onUpdateCheckbox={handleUpdateCheckbox}
onDeleteCheckbox={handleDeleteCheckbox}
onReorderCheckboxes={handleReorderCheckboxes}
onToggleAll={toggleAllToday}
saving={saving}
refreshing={refreshing}
/>
</div> </div>
)} )}
</div> </div>
{/* Layout Tablette/Desktop - Layout original */}
<div className="hidden sm:block">
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* Calendrier - Desktop */}
<div className="xl:col-span-1">
<DailyCalendar
currentDate={currentDate}
onDateSelect={handleDateSelect}
dailyDates={dailyDates}
/>
</div>
{/* Sections daily - Desktop */}
{dailyView && (
<div className="xl:col-span-2 grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Section Hier - Desktop seulement */}
<DailySection
title={getYesterdayTitle()}
date={getYesterdayDate()}
checkboxes={dailyView.yesterday}
onAddCheckbox={handleAddYesterdayCheckbox}
onToggleCheckbox={handleToggleCheckbox}
onUpdateCheckbox={handleUpdateCheckbox}
onDeleteCheckbox={handleDeleteCheckbox}
onReorderCheckboxes={handleReorderCheckboxes}
onToggleAll={toggleAllYesterday}
saving={saving}
refreshing={refreshing}
/>
{/* Section Aujourd'hui - Desktop */}
<DailySection
title={getTodayTitle()}
date={getTodayDate()}
checkboxes={dailyView.today}
onAddCheckbox={handleAddTodayCheckbox}
onToggleCheckbox={handleToggleCheckbox}
onUpdateCheckbox={handleUpdateCheckbox}
onDeleteCheckbox={handleDeleteCheckbox}
onReorderCheckboxes={handleReorderCheckboxes}
onToggleAll={toggleAllToday}
saving={saving}
refreshing={refreshing}
/>
</div>
)}
</div>
</div>
{/* Section des tâches en attente */} {/* Section des tâches en attente */}
<PendingTasksSection <PendingTasksSection
onToggleCheckbox={handleToggleCheckbox} onToggleCheckbox={handleToggleCheckbox}

View File

@@ -11,6 +11,8 @@ import { CreateTaskForm } from '@/components/forms/CreateTaskForm';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { JiraQuickFilter } from '@/components/kanban/JiraQuickFilter'; import { JiraQuickFilter } from '@/components/kanban/JiraQuickFilter';
import { FontSizeToggle } from '@/components/ui/FontSizeToggle'; import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
import { MobileControls } from '@/components/kanban/MobileControls';
import { useIsMobile } from '@/hooks/useIsMobile';
interface KanbanPageClientProps { interface KanbanPageClientProps {
initialTasks: Task[]; initialTasks: Task[];
@@ -21,6 +23,7 @@ function KanbanPageContent() {
const { syncing, createTask, activeFiltersCount, kanbanFilters, setKanbanFilters } = useTasksContext(); const { syncing, createTask, activeFiltersCount, kanbanFilters, setKanbanFilters } = useTasksContext();
const { preferences, updateViewPreferences } = useUserPreferences(); const { preferences, updateViewPreferences } = useUserPreferences();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const isMobile = useIsMobile(768); // Tailwind md breakpoint
// Extraire les préférences du context // Extraire les préférences du context
const showFilters = preferences.viewPreferences.showFilters; const showFilters = preferences.viewPreferences.showFilters;
@@ -59,106 +62,122 @@ function KanbanPageContent() {
syncing={syncing} syncing={syncing}
/> />
{/* Barre de contrôles de visibilité */} {/* Barre de contrôles responsive */}
<div className="bg-[var(--card)]/30 border-b border-[var(--border)]/30"> {isMobile ? (
<div className="container mx-auto px-6 py-2"> <MobileControls
<div className="flex items-center justify-between w-full"> showFilters={showFilters}
<div className="flex items-center gap-4"> showObjectives={showObjectives}
<div className="flex items-center gap-2"> compactView={compactView}
<button activeFiltersCount={activeFiltersCount}
onClick={handleToggleFilters} kanbanFilters={kanbanFilters}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${ onToggleFilters={handleToggleFilters}
showFilters onToggleObjectives={handleToggleObjectives}
? 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30' onToggleCompactView={handleToggleCompactView}
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/50' onFiltersChange={setKanbanFilters}
}`} onCreateTask={() => setIsCreateModalOpen(true)}
> />
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> ) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" /> /* Barre de contrôles desktop */
</svg> <div className="bg-[var(--card)]/30 border-b border-[var(--border)]/30">
Filtres{activeFiltersCount > 0 && ` (${activeFiltersCount})`} <div className="container mx-auto px-6 py-2">
</button> <div className="flex items-center justify-between w-full">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<button
onClick={handleToggleFilters}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
showFilters
? 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/50'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
Filtres{activeFiltersCount > 0 && ` (${activeFiltersCount})`}
</button>
<button
onClick={handleToggleObjectives}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
showObjectives
? 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--accent)]/50'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
Objectifs
</button>
</div>
<div className="flex items-center gap-2 border-l border-[var(--border)] pl-4">
{/* Raccourcis Jira */}
<JiraQuickFilter
filters={kanbanFilters}
onFiltersChange={setKanbanFilters}
/>
<button
onClick={handleToggleCompactView}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
compactView
? 'bg-[var(--secondary)]/20 text-[var(--secondary)] border border-[var(--secondary)]/30'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--secondary)]/50'
}`}
title={compactView ? "Vue détaillée" : "Vue compacte"}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{compactView ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
{compactView ? 'Détaillée' : 'Compacte'}
</button>
<button
onClick={handleToggleSwimlanes}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
swimlanesByTags
? 'bg-[var(--warning)]/20 text-[var(--warning)] border border-[var(--warning)]/30'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--warning)]/50'
}`}
title={swimlanesByTags ? "Vue standard" : "Vue swimlanes"}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{swimlanesByTags ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14-7H5m14 14H5" />
)}
</svg>
{swimlanesByTags ? 'Standard' : 'Swimlanes'}
</button>
{/* Font Size Toggle */}
<FontSizeToggle />
</div>
<button
onClick={handleToggleObjectives}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
showObjectives
? 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--accent)]/50'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
Objectifs
</button>
</div> </div>
<div className="flex items-center gap-2 border-l border-[var(--border)] pl-4">
{/* Raccourcis Jira */}
<JiraQuickFilter
filters={kanbanFilters}
onFiltersChange={setKanbanFilters}
/>
<button
onClick={handleToggleCompactView}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
compactView
? 'bg-[var(--secondary)]/20 text-[var(--secondary)] border border-[var(--secondary)]/30'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--secondary)]/50'
}`}
title={compactView ? "Vue détaillée" : "Vue compacte"}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{compactView ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
{compactView ? 'Détaillée' : 'Compacte'}
</button>
<button
onClick={handleToggleSwimlanes}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${
swimlanesByTags
? 'bg-[var(--warning)]/20 text-[var(--warning)] border border-[var(--warning)]/30'
: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--warning)]/50'
}`}
title={swimlanesByTags ? "Vue standard" : "Vue swimlanes"}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{swimlanesByTags ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14-7H5m14 14H5" />
)}
</svg>
{swimlanesByTags ? 'Standard' : 'Swimlanes'}
</button>
{/* Font Size Toggle */}
<FontSizeToggle />
</div>
</div>
{/* Bouton d'ajout de tâche */} {/* Bouton d'ajout de tâche */}
<Button <Button
variant="primary" variant="primary"
onClick={() => setIsCreateModalOpen(true)} onClick={() => setIsCreateModalOpen(true)}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg> </svg>
Nouvelle tâche Nouvelle tâche
</Button> </Button>
</div>
</div> </div>
</div> </div>
</div> )}
<main className="h-[calc(100vh-160px)]"> <main className="h-[calc(100vh-160px)]">
<KanbanBoardContainer <KanbanBoardContainer

View File

@@ -74,7 +74,7 @@ export function DailyCheckboxItem({
return ( return (
<> <>
<div className={`flex items-center gap-2 px-3 py-1.5 rounded border transition-colors group ${ <div className={`flex items-center gap-3 px-3 py-2 sm:py-1.5 sm:gap-2 rounded border transition-colors group ${
checkbox.type === 'meeting' checkbox.type === 'meeting'
? 'border-l-4 border-l-blue-500 border-t-[var(--border)]/30 border-r-[var(--border)]/30 border-b-[var(--border)]/30 hover:border-t-[var(--border)] hover:border-r-[var(--border)] hover:border-b-[var(--border)]' ? 'border-l-4 border-l-blue-500 border-t-[var(--border)]/30 border-r-[var(--border)]/30 border-b-[var(--border)]/30 hover:border-t-[var(--border)] hover:border-r-[var(--border)] hover:border-b-[var(--border)]'
: 'border-l-4 border-l-green-500 border-t-[var(--border)]/30 border-r-[var(--border)]/30 border-b-[var(--border)]/30 hover:border-t-[var(--border)] hover:border-r-[var(--border)] hover:border-b-[var(--border)]' : 'border-l-4 border-l-green-500 border-t-[var(--border)]/30 border-r-[var(--border)]/30 border-b-[var(--border)]/30 hover:border-t-[var(--border)] hover:border-r-[var(--border)] hover:border-b-[var(--border)]'
@@ -85,7 +85,7 @@ export function DailyCheckboxItem({
checked={checkbox.isChecked} checked={checkbox.isChecked}
onChange={() => onToggle(checkbox.id)} onChange={() => onToggle(checkbox.id)}
disabled={saving} disabled={saving}
className="w-3.5 h-3.5 rounded border border-[var(--border)] text-[var(--primary)] focus:ring-[var(--primary)]/20 focus:ring-1" className="w-4 h-4 md:w-3.5 md:h-3.5 rounded border border-[var(--border)] text-[var(--primary)] focus:ring-[var(--primary)]/20 focus:ring-1"
/> />
{/* Contenu principal */} {/* Contenu principal */}
@@ -102,7 +102,7 @@ export function DailyCheckboxItem({
<div className="flex-1 flex items-center gap-2"> <div className="flex-1 flex items-center gap-2">
{/* Texte cliquable pour édition inline */} {/* Texte cliquable pour édition inline */}
<span <span
className={`flex-1 text-xs font-mono transition-all cursor-pointer hover:bg-[var(--muted)]/50 py-0.5 px-1 rounded ${ className={`flex-1 text-sm sm:text-xs font-mono transition-all cursor-pointer hover:bg-[var(--muted)]/50 py-0.5 px-1 rounded ${
checkbox.isChecked checkbox.isChecked
? 'line-through text-[var(--muted-foreground)]' ? 'line-through text-[var(--muted-foreground)]'
: 'text-[var(--foreground)]' : 'text-[var(--foreground)]'

View File

@@ -87,7 +87,7 @@ export function DailySection({
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
id={`daily-dnd-${title.replace(/[^a-zA-Z0-9]/g, '-')}`} id={`daily-dnd-${title.replace(/[^a-zA-Z0-9]/g, '-')}`}
> >
<Card className="p-0 flex flex-col h-[600px]"> <Card className="p-0 flex flex-col h-[80vh] sm:h-[600px]">
{/* Header */} {/* Header */}
<div className="p-4 pb-0"> <div className="p-4 pb-0">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">

View File

@@ -6,6 +6,7 @@ import { PrioritySwimlanesBoard } from './PrioritySwimlanesBoard';
import { Task, TaskStatus } from '@/lib/types'; import { Task, TaskStatus } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client'; import { CreateTaskData } from '@/clients/tasks-client';
import { KanbanFilters } from './KanbanFilters'; import { KanbanFilters } from './KanbanFilters';
import { useIsMobile } from '@/hooks/useIsMobile';
interface BoardRouterProps { interface BoardRouterProps {
tasks: Task[]; tasks: Task[];
@@ -26,8 +27,13 @@ export function BoardRouter({
visibleStatuses, visibleStatuses,
loading loading
}: BoardRouterProps) { }: BoardRouterProps) {
const isMobile = useIsMobile(768); // Tailwind md breakpoint
// Sur mobile, toujours utiliser le board standard pour une meilleure UX
const shouldUseSwimlanes = kanbanFilters.swimlanesByTags && !isMobile;
// Logique de routage des boards selon les filtres // Logique de routage des boards selon les filtres
if (kanbanFilters.swimlanesByTags) { if (shouldUseSwimlanes) {
if (kanbanFilters.swimlanesMode === 'priority') { if (kanbanFilters.swimlanesMode === 'priority') {
return ( return (
<PrioritySwimlanesBoard <PrioritySwimlanesBoard

View File

@@ -10,6 +10,7 @@ import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config';
import { SORT_OPTIONS } from '@/lib/sort-config'; import { SORT_OPTIONS } from '@/lib/sort-config';
import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { ColumnVisibilityToggle } from './ColumnVisibilityToggle'; import { ColumnVisibilityToggle } from './ColumnVisibilityToggle';
import { useIsMobile } from '@/hooks/useIsMobile';
export interface KanbanFilters { export interface KanbanFilters {
search?: string; search?: string;
@@ -44,6 +45,7 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
const toggleStatusVisibility = onToggleStatusVisibility || toggleColumnVisibility; const toggleStatusVisibility = onToggleStatusVisibility || toggleColumnVisibility;
const [isSortExpanded, setIsSortExpanded] = useState(false); const [isSortExpanded, setIsSortExpanded] = useState(false);
const [isSwimlaneModeExpanded, setIsSwimlaneModeExpanded] = useState(false); const [isSwimlaneModeExpanded, setIsSwimlaneModeExpanded] = useState(false);
const isMobile = useIsMobile(768); // Tailwind md breakpoint
const sortDropdownRef = useRef<HTMLDivElement>(null); const sortDropdownRef = useRef<HTMLDivElement>(null);
const swimlaneModeDropdownRef = useRef<HTMLDivElement>(null); const swimlaneModeDropdownRef = useRef<HTMLDivElement>(null);
const sortButtonRef = useRef<HTMLButtonElement>(null); const sortButtonRef = useRef<HTMLButtonElement>(null);
@@ -262,52 +264,54 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
/> />
</div> </div>
{/* Menu swimlanes */} {/* Menu swimlanes - masqué sur mobile */}
<div className="flex gap-1"> {!isMobile && (
<Button <div className="flex gap-1">
variant={filters.swimlanesByTags ? "primary" : "ghost"}
onClick={handleSwimlanesToggle}
className="flex items-center gap-2"
title="Mode d'affichage"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{filters.swimlanesByTags ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
)}
</svg>
{!filters.swimlanesByTags
? 'Normal'
: filters.swimlanesMode === 'priority'
? 'Par priorité'
: 'Par tags'
}
</Button>
{/* Bouton pour changer le mode des swimlanes */}
{filters.swimlanesByTags && (
<Button <Button
variant="ghost" variant={filters.swimlanesByTags ? "primary" : "ghost"}
onClick={handleSwimlaneModeToggle} onClick={handleSwimlanesToggle}
className="flex items-center gap-1 px-2" className="flex items-center gap-2"
title="Mode d'affichage"
> >
<svg <svg
className={`w-3 h-3 transition-transform ${isSwimlaneModeExpanded ? 'rotate-180' : ''}`} className="w-4 h-4"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> {filters.swimlanesByTags ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
)}
</svg> </svg>
{!filters.swimlanesByTags
? 'Normal'
: filters.swimlanesMode === 'priority'
? 'Par priorité'
: 'Par tags'
}
</Button> </Button>
)}
</div> {/* Bouton pour changer le mode des swimlanes */}
{filters.swimlanesByTags && (
<Button
variant="ghost"
onClick={handleSwimlaneModeToggle}
className="flex items-center gap-1 px-2"
>
<svg
className={`w-3 h-3 transition-transform ${isSwimlaneModeExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</Button>
)}
</div>
)}
{/* Bouton de tri */} {/* Bouton de tri */}
@@ -600,8 +604,8 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
document.body document.body
)} )}
{/* Dropdown des modes swimlanes rendu via portail pour éviter les problèmes de z-index */} {/* Dropdown des modes swimlanes rendu via portail pour éviter les problèmes de z-index - masqué sur mobile */}
{isSwimlaneModeExpanded && typeof window !== 'undefined' && createPortal( {!isMobile && isSwimlaneModeExpanded && typeof window !== 'undefined' && createPortal(
<div <div
ref={swimlaneModeDropdownRef} ref={swimlaneModeDropdownRef}
className="fixed bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl z-[9999] min-w-[140px]" className="fixed bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl z-[9999] min-w-[140px]"

View File

@@ -0,0 +1,165 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { JiraQuickFilter } from '@/components/kanban/JiraQuickFilter';
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
import { KanbanFilters } from '@/components/kanban/KanbanFilters';
interface MobileControlsProps {
showFilters: boolean;
showObjectives: boolean;
compactView: boolean;
activeFiltersCount: number;
kanbanFilters: KanbanFilters;
onToggleFilters: () => void;
onToggleObjectives: () => void;
onToggleCompactView: () => void;
onFiltersChange: (filters: KanbanFilters) => void;
onCreateTask: () => void;
}
export function MobileControls({
showFilters,
showObjectives,
compactView,
activeFiltersCount,
kanbanFilters,
onToggleFilters,
onToggleObjectives,
onToggleCompactView,
onFiltersChange,
onCreateTask,
}: MobileControlsProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<div className="bg-[var(--card)]/30 border-b border-[var(--border)]/30">
<div className="px-4 py-2">
{/* Barre principale mobile */}
<div className="flex items-center justify-between">
{/* Bouton menu hamburger */}
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="flex items-center gap-2 px-3 py-2 rounded-md bg-[var(--card)] border border-[var(--border)] text-[var(--muted-foreground)] hover:border-[var(--primary)]/50 transition-all"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
<span className="text-sm font-mono">Options</span>
{activeFiltersCount > 0 && (
<span className="bg-[var(--primary)]/20 text-[var(--primary)] text-xs px-1.5 py-0.5 rounded-full font-mono">
{activeFiltersCount}
</span>
)}
</button>
{/* Bouton d'ajout de tâche */}
<Button
variant="primary"
onClick={onCreateTask}
className="flex items-center gap-2"
size="sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<span className="hidden xs:inline">Nouvelle</span>
</Button>
</div>
{/* Menu déroulant */}
{isMenuOpen && (
<div className="mt-3 p-3 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg">
{/* Section Affichage */}
<div className="mb-4">
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
Affichage
</h3>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => {
onToggleFilters();
setIsMenuOpen(false);
}}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
showFilters
? 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30'
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
Filtres
</button>
<button
onClick={() => {
onToggleObjectives();
setIsMenuOpen(false);
}}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
showObjectives
? 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30'
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
Objectifs
</button>
</div>
</div>
{/* Section Paramètres */}
<div className="mb-4">
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
Paramètres
</h3>
<div className="space-y-2">
<button
onClick={() => {
onToggleCompactView();
setIsMenuOpen(false);
}}
className={`w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
compactView
? 'bg-[var(--secondary)]/20 text-[var(--secondary)] border border-[var(--secondary)]/30'
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{compactView ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
Vue {compactView ? 'détaillée' : 'compacte'}
</button>
<div className="flex items-center justify-between px-3 py-2 bg-[var(--muted)]/30 border border-[var(--border)] rounded-md">
<span className="text-sm font-mono text-[var(--muted-foreground)]">Taille police</span>
<FontSizeToggle />
</div>
</div>
</div>
{/* Section Jira */}
<div>
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
Raccourcis Jira
</h3>
<JiraQuickFilter
filters={kanbanFilters}
onFiltersChange={onFiltersChange}
/>
</div>
</div>
)}
</div>
</div>
);
}

29
src/hooks/useIsMobile.ts Normal file
View File

@@ -0,0 +1,29 @@
'use client';
import { useState, useEffect } from 'react';
/**
* Hook pour détecter si l'utilisateur est sur mobile
* Utilise un breakpoint à 640px (sm en Tailwind)
*/
export function useIsMobile(breakpoint: number = 640): boolean {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkIsMobile = () => {
setIsMobile(window.innerWidth < breakpoint);
};
// Check initial
checkIsMobile();
// Écouter les changements de taille
window.addEventListener('resize', checkIsMobile);
return () => {
window.removeEventListener('resize', checkIsMobile);
};
}, [breakpoint]);
return isMobile;
}