feat: refactor KanbanFilters to use modular filter components
- Replaced inline priority and tag filtering logic with dedicated `PriorityFilters`, `TagFilters`, `GeneralFilters`, and `ColumnFilters` components for better organization and maintainability. - Optimized layout to enhance responsiveness and user experience by restructuring the filter display into a grid format. - Removed unused code related to previous filtering logic, streamlining the component.
This commit is contained in:
@@ -6,13 +6,15 @@ import { TaskPriority, TaskStatus } from '@/lib/types';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { useTasksContext } from '@/contexts/TasksContext';
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
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 { useIsMobile } from '@/hooks/useIsMobile';
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||||
import { JiraFilters } from './filters/JiraFilters';
|
import { JiraFilters } from './filters/JiraFilters';
|
||||||
import { TfsFilters } from './filters/TfsFilters';
|
import { TfsFilters } from './filters/TfsFilters';
|
||||||
|
import { PriorityFilters } from './filters/PriorityFilters';
|
||||||
|
import { TagFilters } from './filters/TagFilters';
|
||||||
|
import { GeneralFilters } from './filters/GeneralFilters';
|
||||||
|
import { ColumnFilters } from './filters/ColumnFilters';
|
||||||
|
|
||||||
export interface KanbanFilters {
|
export interface KanbanFilters {
|
||||||
search?: string;
|
search?: string;
|
||||||
@@ -44,7 +46,7 @@ interface KanbanFiltersProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsHiddenStatuses, onToggleStatusVisibility }: KanbanFiltersProps) {
|
export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsHiddenStatuses, onToggleStatusVisibility }: KanbanFiltersProps) {
|
||||||
const { tags: availableTags, regularTasks, activeFiltersCount } = useTasksContext();
|
const { regularTasks, activeFiltersCount } = useTasksContext();
|
||||||
const { preferences, toggleColumnVisibility } = useUserPreferences();
|
const { preferences, toggleColumnVisibility } = useUserPreferences();
|
||||||
|
|
||||||
// Utiliser les props si disponibles, sinon utiliser le context
|
// Utiliser les props si disponibles, sinon utiliser le context
|
||||||
@@ -197,41 +199,6 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Calculer les compteurs pour les priorités
|
|
||||||
const priorityCounts = useMemo(() => {
|
|
||||||
const counts: Record<string, number> = {};
|
|
||||||
getAllPriorities().forEach(priority => {
|
|
||||||
counts[priority.key] = regularTasks.filter(task => task.priority === priority.key).length;
|
|
||||||
});
|
|
||||||
return counts;
|
|
||||||
}, [regularTasks]);
|
|
||||||
|
|
||||||
// Calculer les compteurs pour les tags
|
|
||||||
const tagCounts = useMemo(() => {
|
|
||||||
const counts: Record<string, number> = {};
|
|
||||||
availableTags.forEach(tag => {
|
|
||||||
counts[tag.name] = regularTasks.filter(task => task.tags?.includes(tag.name)).length;
|
|
||||||
});
|
|
||||||
return counts;
|
|
||||||
}, [regularTasks, availableTags]);
|
|
||||||
|
|
||||||
const priorityOptions = getAllPriorities().map(priorityConfig => ({
|
|
||||||
value: priorityConfig.key,
|
|
||||||
label: priorityConfig.label,
|
|
||||||
color: priorityConfig.color,
|
|
||||||
count: priorityCounts[priorityConfig.key] || 0
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Trier les tags par nombre d'utilisation (décroissant)
|
|
||||||
const sortedTags = useMemo(() => {
|
|
||||||
return [...availableTags].sort((a, b) => {
|
|
||||||
const countA = tagCounts[a.name] || 0;
|
|
||||||
const countB = tagCounts[b.name] || 0;
|
|
||||||
return countB - countA; // Décroissant
|
|
||||||
});
|
|
||||||
}, [availableTags, tagCounts]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[var(--card)]/50 border-b border-[var(--border)]/50 backdrop-blur-sm">
|
<div className="bg-[var(--card)]/50 border-b border-[var(--border)]/50 backdrop-blur-sm">
|
||||||
<div className="container mx-auto px-6 py-4">
|
<div className="container mx-auto px-6 py-4">
|
||||||
@@ -339,110 +306,47 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
|||||||
|
|
||||||
{/* Filtres étendus */}
|
{/* Filtres étendus */}
|
||||||
<div className="mt-4 border-t border-[var(--border)]/50 pt-4">
|
<div className="mt-4 border-t border-[var(--border)]/50 pt-4">
|
||||||
{/* Grille responsive pour les filtres principaux */}
|
{/* Layout optimisé : 3 colonnes avec Tags très large à droite */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-6 lg:gap-8">
|
<div className="grid grid-cols-1 xl:grid-cols-[auto_minmax(0,500px)_3fr] gap-4 lg:gap-6 items-start">
|
||||||
{/* Filtres par priorité */}
|
{/* Colonne 1 : Priorités + Généraux */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
|
<PriorityFilters
|
||||||
Priorités
|
selectedPriorities={filters.priorities}
|
||||||
</label>
|
onPriorityToggle={handlePriorityToggle}
|
||||||
<div className="flex flex-wrap gap-1">
|
/>
|
||||||
{priorityOptions.filter(priority => priority.count > 0).map((priority) => (
|
|
||||||
<button
|
<GeneralFilters
|
||||||
key={priority.value}
|
showWithDueDate={filters.showWithDueDate}
|
||||||
onClick={() => handlePriorityToggle(priority.value)}
|
onDueDateFilterToggle={handleDueDateFilterToggle}
|
||||||
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
|
/>
|
||||||
filters.priorities?.includes(priority.value)
|
|
||||||
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
|
|
||||||
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 rounded-full"
|
|
||||||
style={{ backgroundColor: getPriorityColorHex(priority.color) }}
|
|
||||||
/>
|
|
||||||
{priority.label} ({priority.count})
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filtres par tags */}
|
{/* Colonne 2 : Tags - Espace restant maximum */}
|
||||||
{availableTags.length > 0 && (
|
<TagFilters
|
||||||
<div className="space-y-3">
|
selectedTags={filters.tags}
|
||||||
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
|
onTagToggle={handleTagToggle}
|
||||||
Tags
|
/>
|
||||||
</label>
|
|
||||||
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
|
|
||||||
{sortedTags.filter(tag => (tagCounts[tag.name] || 0) > 0).map((tag) => (
|
|
||||||
<button
|
|
||||||
key={tag.id}
|
|
||||||
onClick={() => handleTagToggle(tag.name)}
|
|
||||||
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
|
|
||||||
filters.tags?.includes(tag.name)
|
|
||||||
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
|
|
||||||
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 rounded-full"
|
|
||||||
style={{ backgroundColor: tag.color }}
|
|
||||||
/>
|
|
||||||
{tag.name} ({tagCounts[tag.name]})
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filtres généraux */}
|
{/* Colonne 3 : Visibilité des colonnes */}
|
||||||
<div className="border-t border-[var(--border)]/30 pt-4 mt-4">
|
<ColumnFilters
|
||||||
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider mb-3">
|
|
||||||
Filtres généraux
|
|
||||||
</label>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleDueDateFilterToggle}
|
|
||||||
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium cursor-pointer ${
|
|
||||||
filters.showWithDueDate
|
|
||||||
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
|
|
||||||
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)] hover:bg-[var(--card)]/80'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
Avec date de fin
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filtres Jira */}
|
|
||||||
<JiraFilters
|
|
||||||
filters={filters}
|
|
||||||
onFiltersChange={onFiltersChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Filtres TFS */}
|
|
||||||
<TfsFilters
|
|
||||||
filters={filters}
|
|
||||||
onFiltersChange={onFiltersChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Visibilité des colonnes */}
|
|
||||||
<div className="col-span-full border-t border-[var(--border)]/50 pt-6 mt-4">
|
|
||||||
<ColumnVisibilityToggle
|
|
||||||
hiddenStatuses={hiddenStatuses}
|
hiddenStatuses={hiddenStatuses}
|
||||||
onToggleStatus={toggleStatusVisibility}
|
onToggleStatus={toggleStatusVisibility}
|
||||||
tasks={regularTasks}
|
tasks={regularTasks}
|
||||||
className="text-xs"
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Deuxième ligne : TFS et Jira côte à côte */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-2">
|
||||||
|
{/* Filtres TFS */}
|
||||||
|
<TfsFilters
|
||||||
|
filters={filters}
|
||||||
|
onFiltersChange={onFiltersChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Filtres Jira */}
|
||||||
|
<JiraFilters
|
||||||
|
filters={filters}
|
||||||
|
onFiltersChange={onFiltersChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
39
src/components/kanban/filters/ColumnFilters.tsx
Normal file
39
src/components/kanban/filters/ColumnFilters.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { TaskStatus, Task } from '@/lib/types';
|
||||||
|
import { getAllStatuses } from '@/lib/status-config';
|
||||||
|
|
||||||
|
interface ColumnFiltersProps {
|
||||||
|
hiddenStatuses: Set<TaskStatus>;
|
||||||
|
onToggleStatus: (status: TaskStatus) => void;
|
||||||
|
tasks: Task[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ColumnFilters({ hiddenStatuses, onToggleStatus, tasks }: ColumnFiltersProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
|
||||||
|
Colonnes
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{getAllStatuses().map(statusConfig => {
|
||||||
|
const statusCount = tasks.filter(task => task.status === statusConfig.key).length;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={statusConfig.key}
|
||||||
|
onClick={() => onToggleStatus(statusConfig.key)}
|
||||||
|
className={`px-2 py-1 rounded border text-xs font-medium transition-colors ${
|
||||||
|
hiddenStatuses.has(statusConfig.key)
|
||||||
|
? 'bg-[var(--muted)]/20 text-[var(--muted)] border-[var(--muted)]/30 hover:bg-[var(--muted)]/30'
|
||||||
|
: 'bg-[var(--primary)]/20 text-[var(--primary)] border-[var(--primary)]/30 hover:bg-[var(--primary)]/30'
|
||||||
|
}`}
|
||||||
|
title={hiddenStatuses.has(statusConfig.key) ? `Afficher ${statusConfig.label}` : `Masquer ${statusConfig.label}`}
|
||||||
|
>
|
||||||
|
{hiddenStatuses.has(statusConfig.key) ? '👁️🗨️' : '👁️'} {statusConfig.label}{statusCount ? ` (${statusCount})` : ''}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
src/components/kanban/filters/GeneralFilters.tsx
Normal file
37
src/components/kanban/filters/GeneralFilters.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
interface GeneralFiltersProps {
|
||||||
|
showWithDueDate?: boolean;
|
||||||
|
onDueDateFilterToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GeneralFilters({ showWithDueDate = false, onDueDateFilterToggle }: GeneralFiltersProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
|
||||||
|
Généraux
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onDueDateFilterToggle}
|
||||||
|
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium cursor-pointer ${
|
||||||
|
showWithDueDate
|
||||||
|
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
|
||||||
|
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)] hover:bg-[var(--card)]/80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 002 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Avec date de fin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/components/kanban/filters/PriorityFilters.tsx
Normal file
58
src/components/kanban/filters/PriorityFilters.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { TaskPriority } from '@/lib/types';
|
||||||
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
|
import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config';
|
||||||
|
|
||||||
|
interface PriorityFiltersProps {
|
||||||
|
selectedPriorities?: TaskPriority[];
|
||||||
|
onPriorityToggle: (priority: TaskPriority) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PriorityFilters({ selectedPriorities = [], onPriorityToggle }: PriorityFiltersProps) {
|
||||||
|
const { regularTasks } = useTasksContext();
|
||||||
|
|
||||||
|
// Calculer les compteurs pour les priorités
|
||||||
|
const priorityCounts = useMemo(() => {
|
||||||
|
const counts: Record<string, number> = {};
|
||||||
|
getAllPriorities().forEach(priority => {
|
||||||
|
counts[priority.key] = regularTasks.filter(task => task.priority === priority.key).length;
|
||||||
|
});
|
||||||
|
return counts;
|
||||||
|
}, [regularTasks]);
|
||||||
|
|
||||||
|
const priorityOptions = getAllPriorities().map(priorityConfig => ({
|
||||||
|
value: priorityConfig.key,
|
||||||
|
label: priorityConfig.label,
|
||||||
|
color: priorityConfig.color,
|
||||||
|
count: priorityCounts[priorityConfig.key] || 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
|
||||||
|
Priorités
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{priorityOptions.filter(priority => priority.count > 0).map((priority) => (
|
||||||
|
<button
|
||||||
|
key={priority.value}
|
||||||
|
onClick={() => onPriorityToggle(priority.value)}
|
||||||
|
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
|
||||||
|
selectedPriorities.includes(priority.value)
|
||||||
|
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
|
||||||
|
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: getPriorityColorHex(priority.color) }}
|
||||||
|
/>
|
||||||
|
{priority.label} ({priority.count})
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
62
src/components/kanban/filters/TagFilters.tsx
Normal file
62
src/components/kanban/filters/TagFilters.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
|
|
||||||
|
interface TagFiltersProps {
|
||||||
|
selectedTags?: string[];
|
||||||
|
onTagToggle: (tagName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TagFilters({ selectedTags = [], onTagToggle }: TagFiltersProps) {
|
||||||
|
const { tags: availableTags, regularTasks } = useTasksContext();
|
||||||
|
|
||||||
|
// Calculer les compteurs pour les tags
|
||||||
|
const tagCounts = useMemo(() => {
|
||||||
|
const counts: Record<string, number> = {};
|
||||||
|
availableTags.forEach(tag => {
|
||||||
|
counts[tag.name] = regularTasks.filter(task => task.tags?.includes(tag.name)).length;
|
||||||
|
});
|
||||||
|
return counts;
|
||||||
|
}, [regularTasks, availableTags]);
|
||||||
|
|
||||||
|
// Trier les tags par nombre d'utilisation (décroissant)
|
||||||
|
const sortedTags = useMemo(() => {
|
||||||
|
return [...availableTags].sort((a, b) => {
|
||||||
|
const countA = tagCounts[a.name] || 0;
|
||||||
|
const countB = tagCounts[b.name] || 0;
|
||||||
|
return countB - countA; // Décroissant
|
||||||
|
});
|
||||||
|
}, [availableTags, tagCounts]);
|
||||||
|
|
||||||
|
if (availableTags.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
|
||||||
|
Tags
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
|
||||||
|
{sortedTags.filter(tag => (tagCounts[tag.name] || 0) > 0).map((tag) => (
|
||||||
|
<button
|
||||||
|
key={tag.id}
|
||||||
|
onClick={() => onTagToggle(tag.name)}
|
||||||
|
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
|
||||||
|
selectedTags.includes(tag.name)
|
||||||
|
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
|
||||||
|
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: tag.color }}
|
||||||
|
/>
|
||||||
|
{tag.name} ({tagCounts[tag.name]})
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user