176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useMemo } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandList,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { CategoryIcon } from "@/components/ui/category-icon";
|
|
import { ChevronsUpDown } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
// Group icons by category for better organization
|
|
const iconGroups: Record<string, string[]> = {
|
|
"Alimentation": [
|
|
"shopping-cart", "utensils", "croissant", "coffee", "wine", "beer",
|
|
"pizza", "apple", "cherry", "salad", "sandwich", "ice-cream",
|
|
"cake", "cup-soda", "milk", "egg", "fish", "beef"
|
|
],
|
|
"Transport": [
|
|
"fuel", "train", "car", "parking", "bike", "plane", "bus",
|
|
"ship", "sailboat", "truck", "car-front", "circle-parking",
|
|
"train-front"
|
|
],
|
|
"Logement": [
|
|
"home", "zap", "droplet", "hammer", "sofa", "refrigerator",
|
|
"washing-machine", "lamp", "lamp-desk", "armchair", "bath",
|
|
"shower-head", "door-open", "fence", "trees", "flower",
|
|
"leaf", "sun", "snowflake", "wind", "thermometer"
|
|
],
|
|
"Santé": [
|
|
"pill", "stethoscope", "hospital", "glasses", "dumbbell", "sparkles",
|
|
"heart", "heart-pulse", "activity", "syringe", "bandage", "brain",
|
|
"eye", "ear", "hand", "footprints", "person-standing"
|
|
],
|
|
"Loisirs": [
|
|
"tv", "music", "film", "gamepad", "book", "ticket", "clapperboard",
|
|
"headphones", "speaker", "radio", "camera", "image", "palette",
|
|
"brush", "pen-tool", "scissors", "drama", "party-popper"
|
|
],
|
|
"Sport": ["trophy", "medal", "target", "volleyball"],
|
|
"Shopping": [
|
|
"shirt", "smartphone", "package", "shopping-bag", "store", "gem",
|
|
"watch", "sunglasses", "crown", "laptop", "monitor", "keyboard",
|
|
"mouse", "printer", "tablet-smartphone", "headset"
|
|
],
|
|
"Services": [
|
|
"wifi", "repeat", "landmark", "shield", "receipt", "file-text",
|
|
"mail", "phone", "message-square", "send", "globe", "cloud",
|
|
"server", "lock", "unlock", "settings", "wrench"
|
|
],
|
|
"Finance": [
|
|
"piggy-bank", "banknote", "wallet", "hand-coins", "undo", "coins",
|
|
"credit-card", "building", "building2", "trending-up", "trending-down",
|
|
"bar-chart", "pie-chart", "line-chart", "calculator", "percent",
|
|
"dollar-sign", "euro"
|
|
],
|
|
"Voyage": [
|
|
"bed", "luggage", "map", "map-pin", "compass", "mountain",
|
|
"tent", "palmtree", "umbrella", "globe2", "flag"
|
|
],
|
|
"Famille": [
|
|
"graduation-cap", "baby", "paw-print", "users", "user", "user-plus",
|
|
"dog", "cat", "bird", "rabbit"
|
|
],
|
|
"Autre": [
|
|
"heart-handshake", "gift", "cigarette", "arrow-right-left",
|
|
"help-circle", "tag", "folder", "key", "star", "bookmark", "clock",
|
|
"calendar", "bell", "alert-triangle", "info", "check-circle", "x-circle",
|
|
"plus", "minus", "search", "trash", "edit", "download", "upload",
|
|
"share", "link", "paperclip", "archive", "box", "boxes", "container",
|
|
"briefcase", "education", "award", "lightbulb", "flame", "rocket", "atom"
|
|
],
|
|
};
|
|
|
|
interface IconPickerProps {
|
|
value: string;
|
|
onChange: (icon: string) => void;
|
|
color?: string;
|
|
}
|
|
|
|
export function IconPicker({ value, onChange, color }: IconPickerProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const [search, setSearch] = useState("");
|
|
|
|
// Filter icons based on search
|
|
const filteredGroups = useMemo(() => {
|
|
if (!search.trim()) return iconGroups;
|
|
|
|
const query = search.toLowerCase();
|
|
const result: Record<string, string[]> = {};
|
|
|
|
Object.entries(iconGroups).forEach(([group, icons]) => {
|
|
const filtered = icons.filter(
|
|
(icon) =>
|
|
icon.toLowerCase().includes(query) ||
|
|
group.toLowerCase().includes(query)
|
|
);
|
|
if (filtered.length > 0) {
|
|
result[group] = filtered;
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}, [search]);
|
|
|
|
const handleSelect = (icon: string) => {
|
|
onChange(icon);
|
|
setOpen(false);
|
|
setSearch("");
|
|
};
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
className="w-full justify-between"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<CategoryIcon icon={value} color={color} size={20} />
|
|
<span className="text-muted-foreground text-sm">{value}</span>
|
|
</div>
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="w-[350px] p-0"
|
|
align="start"
|
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
>
|
|
<Command>
|
|
<CommandInput
|
|
placeholder="Rechercher une icône..."
|
|
value={search}
|
|
onValueChange={setSearch}
|
|
/>
|
|
<CommandList className="max-h-[300px]">
|
|
<CommandEmpty>Aucune icône trouvée.</CommandEmpty>
|
|
{Object.entries(filteredGroups).map(([group, icons]) => (
|
|
<CommandGroup key={group} heading={group}>
|
|
<div className="grid grid-cols-6 gap-1 p-1">
|
|
{icons.map((icon) => (
|
|
<button
|
|
key={icon}
|
|
onClick={() => handleSelect(icon)}
|
|
className={cn(
|
|
"flex items-center justify-center p-2 rounded-md hover:bg-accent transition-colors",
|
|
value === icon && "bg-accent ring-2 ring-primary"
|
|
)}
|
|
title={icon}
|
|
>
|
|
<CategoryIcon icon={icon} color={color} size={20} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</CommandGroup>
|
|
))}
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|
|
|