From cc1e8c20a6fb6bc1ca8b881c8007da74c63547cf Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 27 Nov 2025 10:46:07 +0100 Subject: [PATCH] feat: add search functionality and expand/collapse controls to categories page for improved user experience --- app/categories/page.tsx | 73 ++++++++++++++++++++++++++++++++++++++--- lib/defaults.ts | 30 ++++++++--------- 2 files changed, 84 insertions(+), 19 deletions(-) diff --git a/app/categories/page.tsx b/app/categories/page.tsx index ad6ee9c..b492b37 100644 --- a/app/categories/page.tsx +++ b/app/categories/page.tsx @@ -11,7 +11,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Plus, MoreVertical, Pencil, Trash2, RefreshCw, X, ChevronDown, ChevronRight } from "lucide-react" +import { Plus, MoreVertical, Pencil, Trash2, RefreshCw, X, ChevronDown, ChevronRight, ChevronsUpDown, Search } from "lucide-react" import { CategoryIcon } from "@/components/ui/category-icon" import { autoCategorize, addCategory, updateCategory, deleteCategory } from "@/lib/store-db" import type { Category } from "@/lib/types" @@ -35,6 +35,7 @@ export default function CategoriesPage() { parentId: null as string | null, }) const [newKeyword, setNewKeyword] = useState("") + const [searchQuery, setSearchQuery] = useState("") // Organiser les catégories par parent const { parentCategories, childrenByParent, orphanCategories } = useMemo(() => { @@ -111,6 +112,16 @@ export default function CategoriesPage() { setExpandedParents(newExpanded) } + const expandAll = () => { + setExpandedParents(new Set(parentCategories.map((p) => p.id))) + } + + const collapseAll = () => { + setExpandedParents(new Set()) + } + + const allExpanded = parentCategories.length > 0 && expandedParents.size === parentCategories.length + const handleNewCategory = (parentId: string | null = null) => { setEditingCategory(null) setFormData({ name: "", color: "#22c55e", keywords: [], parentId }) @@ -282,12 +293,66 @@ export default function CategoriesPage() { + {/* Barre de recherche et contrôles */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> + {searchQuery && ( + + )} +
+ +
+ {/* Liste des catégories par parent */}
- {parentCategories.map((parent) => { - const children = childrenByParent[parent.id] || [] + {parentCategories + .filter((parent) => { + if (!searchQuery.trim()) return true + const query = searchQuery.toLowerCase() + // Afficher si le parent matche + if (parent.name.toLowerCase().includes(query)) return true + if (parent.keywords.some((k) => k.toLowerCase().includes(query))) return true + // Ou si un enfant matche + const children = childrenByParent[parent.id] || [] + return children.some( + (c) => + c.name.toLowerCase().includes(query) || + c.keywords.some((k) => k.toLowerCase().includes(query)) + ) + }) + .map((parent) => { + const allChildren = childrenByParent[parent.id] || [] + // Filtrer les enfants aussi si recherche active + const children = searchQuery.trim() + ? allChildren.filter( + (c) => + c.name.toLowerCase().includes(searchQuery.toLowerCase()) || + c.keywords.some((k) => k.toLowerCase().includes(searchQuery.toLowerCase())) || + // Garder tous les enfants si le parent matche + parent.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + : allChildren const stats = getCategoryStats(parent.id, true) - const isExpanded = expandedParents.has(parent.id) + const isExpanded = expandedParents.has(parent.id) || (searchQuery.trim() !== "" && children.length > 0) return (
diff --git a/lib/defaults.ts b/lib/defaults.ts index fcf53d8..5827378 100644 --- a/lib/defaults.ts +++ b/lib/defaults.ts @@ -18,7 +18,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "alimentation", - name: "🛒 Alimentation", + name: "Alimentation", color: "#22c55e", icon: "utensils", keywords: [], @@ -84,7 +84,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "transport", - name: "🚗 Transport", + name: "Transport", color: "#3b82f6", icon: "car", keywords: [], @@ -201,7 +201,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "logement", - name: "🏠 Logement", + name: "Logement", color: "#f59e0b", icon: "home", keywords: [], @@ -291,7 +291,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "sante", - name: "💊 Santé", + name: "Santé", color: "#ef4444", icon: "heart", keywords: [], @@ -385,7 +385,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "loisirs", - name: "🎬 Loisirs", + name: "Loisirs", color: "#8b5cf6", icon: "gamepad", keywords: [], @@ -482,7 +482,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "shopping", - name: "👕 Shopping", + name: "Shopping", color: "#06b6d4", icon: "shopping-bag", keywords: [], @@ -548,7 +548,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "abonnements", - name: "📱 Abonnements", + name: "Abonnements", color: "#8b5cf6", icon: "repeat", keywords: [], @@ -592,7 +592,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "finance", - name: "💰 Finance", + name: "Finance", color: "#64748b", icon: "landmark", keywords: [], @@ -691,7 +691,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "revenus", - name: "💵 Revenus", + name: "Revenus", color: "#10b981", icon: "wallet", keywords: [], @@ -749,7 +749,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "voyage", - name: "✈️ Voyage", + name: "Voyage", color: "#a855f7", icon: "plane", keywords: [], @@ -793,7 +793,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "education", - name: "📚 Éducation", + name: "Éducation", color: "#0284c7", icon: "graduation-cap", keywords: [], @@ -836,7 +836,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "animaux", - name: "🐕 Animaux", + name: "Animaux", color: "#ea580c", icon: "paw-print", keywords: [ @@ -857,7 +857,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "auto", - name: "🔧 Auto & Moto", + name: "Auto & Moto", color: "#78716c", icon: "car", keywords: [], @@ -899,7 +899,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "dons", - name: "🎁 Dons & Cadeaux", + name: "Dons & Cadeaux", color: "#f43f5e", icon: "gift", keywords: [], @@ -935,7 +935,7 @@ export const defaultCategories: CategoryDefinition[] = [ // ═══════════════════════════════════════════════════════════════════════════ { slug: "divers", - name: "❓ Divers", + name: "Divers", color: "#71717a", icon: "more-horizontal", keywords: [],