From 7064c0b0bcb28569ee73ecc4f295e0913460cdcf Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Tue, 17 Mar 2026 17:08:11 +0000 Subject: [PATCH] =?UTF-8?q?feat(categorias):=20adiciona=20seletor=20pesqui?= =?UTF-8?q?s=C3=A1vel=20de=20=C3=ADcones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/category-form-fields.tsx | 84 ++++---- .../categories/components/category-icon.tsx | 12 +- .../components/category-picker-dialog.tsx | 117 ++++++++++++ src/shared/lib/categories/icons.ts | 180 ++++++++++++++++++ 4 files changed, 337 insertions(+), 56 deletions(-) create mode 100644 src/features/categories/components/category-picker-dialog.tsx diff --git a/src/features/categories/components/category-form-fields.tsx b/src/features/categories/components/category-form-fields.tsx index 04a02f9..11e3f10 100644 --- a/src/features/categories/components/category-form-fields.tsx +++ b/src/features/categories/components/category-form-fields.tsx @@ -1,15 +1,9 @@ "use client"; import { RiMoreLine } from "@remixicon/react"; -import { useState } from "react"; -import { Button } from "@/shared/components/ui/button"; +import { useMemo, useState } from "react"; import { Input } from "@/shared/components/ui/input"; import { Label } from "@/shared/components/ui/label"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/shared/components/ui/popover"; import { Select, SelectContent, @@ -25,6 +19,7 @@ import { getCategoryIconOptions } from "@/shared/lib/categories/icons"; import { cn } from "@/shared/utils/ui"; import { CategoryIcon } from "./category-icon"; +import { CategoryPickerDialog } from "./category-picker-dialog"; import { TypeSelectContent } from "./category-select-items"; import type { CategoryFormValues } from "./types"; @@ -33,17 +28,17 @@ interface CategoryFormFieldsProps { onChange: (field: keyof CategoryFormValues, value: string) => void; } +const iconOptions = getCategoryIconOptions(); + export function CategoryFormFields({ values, onChange, }: CategoryFormFieldsProps) { - const [popoverOpen, setPopoverOpen] = useState(false); - const iconOptions = getCategoryIconOptions(); + const [pickerOpen, setPickerOpen] = useState(false); - const handleIconSelect = (icon: string) => { - onChange("icon", icon); - setPopoverOpen(false); - }; + const selectedIconLabel = useMemo(() => { + return iconOptions.find((o) => o.value === values.icon)?.label ?? null; + }, [values.icon]); return (
@@ -83,45 +78,36 @@ export function CategoryFormFields({
-
-
+
- - - - - -
- {iconOptions.map((option) => ( - - ))} -
-
-
-
-

- Escolha um ícone que represente melhor esta categoria. -

+ + + + {selectedIconLabel ?? "Selecionar ícone"} + + + Clique para trocar o ícone + + + + + onChange("icon", icon)} + />
); diff --git a/src/features/categories/components/category-icon.tsx b/src/features/categories/components/category-icon.tsx index 23712c8..f34aad5 100644 --- a/src/features/categories/components/category-icon.tsx +++ b/src/features/categories/components/category-icon.tsx @@ -1,20 +1,18 @@ "use client"; -import type { RemixiconComponentType } from "@remixicon/react"; -import * as RemixIcons from "@remixicon/react"; +import type { ComponentType } from "react"; +import { getIconComponent } from "@/shared/utils/icons"; import { cn } from "@/shared/utils/ui"; -const ICONS = RemixIcons as Record; -const FALLBACK_ICON = ICONS.RiPriceTag3Line; - interface CategoryIconProps { name?: string | null; className?: string; } export function CategoryIcon({ name, className }: CategoryIconProps) { - const IconComponent = - (name ? ICONS[name] : undefined) ?? FALLBACK_ICON ?? null; + const IconComponent = ( + name ? getIconComponent(name) : getIconComponent("RiPriceTag3Line") + ) as ComponentType<{ className?: string }> | null; if (!IconComponent) { return ( diff --git a/src/features/categories/components/category-picker-dialog.tsx b/src/features/categories/components/category-picker-dialog.tsx new file mode 100644 index 0000000..91f73da --- /dev/null +++ b/src/features/categories/components/category-picker-dialog.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/shared/components/ui/dialog"; +import { Input } from "@/shared/components/ui/input"; +import { CATEGORY_ICON_GROUPS } from "@/shared/lib/categories/icons"; +import { cn } from "@/shared/utils/ui"; + +import { CategoryIcon } from "./category-icon"; + +interface CategoryPickerDialogProps { + open: boolean; + value: string; + onOpenChange: (open: boolean) => void; + onSelect: (icon: string) => void; +} + +export function CategoryPickerDialog({ + open, + value, + onOpenChange, + onSelect, +}: CategoryPickerDialogProps) { + const [search, setSearch] = useState(""); + + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) setSearch(""); + onOpenChange(isOpen); + }; + + const filteredGroups = useMemo(() => { + const query = search.toLowerCase().trim(); + if (!query) return CATEGORY_ICON_GROUPS; + + return CATEGORY_ICON_GROUPS.flatMap((group) => { + const icons = group.icons.filter( + (icon) => + icon.label.toLowerCase().includes(query) || + group.label.toLowerCase().includes(query), + ); + return icons.length > 0 ? [{ ...group, icons }] : []; + }); + }, [search]); + + const totalVisible = filteredGroups.reduce( + (acc, g) => acc + g.icons.length, + 0, + ); + + return ( + + + + Escolher ícone + + Selecione o ícone que melhor representa esta categoria. + + + + setSearch(e.target.value)} + className="h-8 text-sm" + autoFocus + /> + + {totalVisible === 0 ? ( +

+ Nenhum ícone encontrado para “{search}” +

+ ) : ( +
+ {filteredGroups.map((group) => ( +
+

+ {group.label} +

+
+ {group.icons.map((option) => ( + + ))} +
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/shared/lib/categories/icons.ts b/src/shared/lib/categories/icons.ts index d73cf39..abe8a3d 100644 --- a/src/shared/lib/categories/icons.ts +++ b/src/shared/lib/categories/icons.ts @@ -156,6 +156,186 @@ export const CATEGORY_ICON_OPTIONS: CategoryIconOption[] = [ { label: "Nuvem Upload", value: "RiCloudUploadLine" }, ]; +export type CategoryIconGroup = { + label: string; + icons: CategoryIconOption[]; +}; + +export const CATEGORY_ICON_GROUPS: CategoryIconGroup[] = [ + { + label: "Finanças", + icons: [ + { label: "Dinheiro", value: "RiMoneyDollarCircleLine" }, + { label: "Carteira", value: "RiWallet3Line" }, + { label: "Carteira 2", value: "RiWalletLine" }, + { label: "Cartão", value: "RiBankCard2Line" }, + { label: "Banco", value: "RiBankLine" }, + { label: "Moedas", value: "RiHandCoinLine" }, + { label: "Gráfico", value: "RiLineChartLine" }, + { label: "Ações", value: "RiStockLine" }, + { label: "Troca", value: "RiExchangeLine" }, + { label: "Reembolso", value: "RiRefundLine" }, + { label: "Recompensa", value: "RiRefund2Line" }, + { label: "Leilão", value: "RiAuctionLine" }, + ], + }, + { + label: "Compras", + icons: [ + { label: "Carrinho", value: "RiShoppingCartLine" }, + { label: "Sacola", value: "RiShoppingBagLine" }, + { label: "Cesta", value: "RiShoppingBasketLine" }, + { label: "Presente", value: "RiGiftLine" }, + { label: "Cupom", value: "RiCouponLine" }, + { label: "Ticket", value: "RiTicket2Line" }, + ], + }, + { + label: "Alimentação", + icons: [ + { label: "Restaurante", value: "RiRestaurantLine" }, + { label: "Garfo e faca", value: "RiRestaurant2Line" }, + { label: "Café", value: "RiCupLine" }, + { label: "Bebida", value: "RiDrinksFill" }, + { label: "Pizza", value: "RiCake3Line" }, + { label: "Cerveja", value: "RiBeerLine" }, + ], + }, + { + label: "Transporte", + icons: [ + { label: "Ônibus", value: "RiBusLine" }, + { label: "Carro", value: "RiCarLine" }, + { label: "Táxi", value: "RiTaxiLine" }, + { label: "Moto", value: "RiMotorbikeLine" }, + { label: "Avião", value: "RiFlightTakeoffLine" }, + { label: "Navio", value: "RiShipLine" }, + { label: "Trem", value: "RiTrainLine" }, + { label: "Metrô", value: "RiSubwayLine" }, + { label: "Bicicleta", value: "RiBikeLine" }, + { label: "Mapa", value: "RiMapPinLine" }, + { label: "Combustível", value: "RiGasStationLine" }, + ], + }, + { + label: "Moradia", + icons: [ + { label: "Casa", value: "RiHomeLine" }, + { label: "Prédio", value: "RiBuilding2Line" }, + { label: "Apartamento", value: "RiBuildingLine" }, + { label: "Ferramentas", value: "RiToolsLine" }, + { label: "Lâmpada", value: "RiLightbulbLine" }, + { label: "Energia", value: "RiFlashlightLine" }, + ], + }, + { + label: "Saúde e bem-estar", + icons: [ + { label: "Saúde", value: "RiStethoscopeLine" }, + { label: "Hospital", value: "RiHospitalLine" }, + { label: "Coração", value: "RiHeart2Line" }, + { label: "Pulso", value: "RiHeartPulseLine" }, + { label: "Mental", value: "RiMentalHealthLine" }, + { label: "Farmácia", value: "RiFirstAidKitLine" }, + { label: "Fitness", value: "RiRunLine" }, + ], + }, + { + label: "Educação", + icons: [ + { label: "Livro", value: "RiBook2Line" }, + { label: "Graduação", value: "RiGraduationCapLine" }, + { label: "Escola", value: "RiSchoolLine" }, + { label: "Lápis", value: "RiPencilLine" }, + ], + }, + { + label: "Trabalho", + icons: [ + { label: "Maleta", value: "RiBriefcaseLine" }, + { label: "Pasta", value: "RiBriefcase4Line" }, + { label: "Escritório", value: "RiUserStarLine" }, + ], + }, + { + label: "Lazer", + icons: [ + { label: "Controle", value: "RiGamepadLine" }, + { label: "Filme", value: "RiMovie2Line" }, + { label: "Música", value: "RiMusic2Line" }, + { label: "Microfone", value: "RiMicLine" }, + { label: "Fone", value: "RiHeadphoneLine" }, + { label: "Câmera", value: "RiCameraLine" }, + { label: "Praia", value: "RiUmbrellaLine" }, + { label: "Futebol", value: "RiFootballLine" }, + { label: "Basquete", value: "RiBasketballLine" }, + ], + }, + { + label: "Tecnologia", + icons: [ + { label: "WiFi", value: "RiWifiLine" }, + { label: "Celular", value: "RiSmartphoneLine" }, + { label: "Computador", value: "RiComputerLine" }, + { label: "Monitor", value: "RiMonitorLine" }, + { label: "Teclado", value: "RiKeyboardLine" }, + { label: "Mouse", value: "RiMouseLine" }, + { label: "Fone Bluetooth", value: "RiBluetoothLine" }, + ], + }, + { + label: "Pessoas", + icons: [ + { label: "Usuário", value: "RiUserLine" }, + { label: "Grupo", value: "RiGroupLine" }, + { label: "Família", value: "RiParentLine" }, + { label: "Bebê", value: "RiBabyCarriageLine" }, + ], + }, + { + label: "Outros", + icons: [ + { label: "Animais", value: "RiBearSmileLine" }, + { label: "Camiseta", value: "RiTShirtLine" }, + { label: "Arquivo", value: "RiFileTextLine" }, + { label: "Documento", value: "RiArticleLine" }, + { label: "Balança", value: "RiScales2Line" }, + { label: "Escudo", value: "RiShieldCheckLine" }, + { label: "Serviço", value: "RiServiceLine" }, + { label: "Alerta", value: "RiAlertLine" }, + { label: "Troféu", value: "RiMedalLine" }, + { label: "Mais", value: "RiMore2Line" }, + { label: "Estrela", value: "RiStarLine" }, + { label: "Foguete", value: "RiRocketLine" }, + { label: "Ampulheta", value: "RiHourglassLine" }, + { label: "Calendário", value: "RiCalendarLine" }, + { label: "Relógio", value: "RiTimeLine" }, + { label: "Timer", value: "RiTimer2Line" }, + { label: "Fogo", value: "RiFireLine" }, + { label: "Gota", value: "RiDropLine" }, + { label: "Sol", value: "RiSunLine" }, + { label: "Lua", value: "RiMoonLine" }, + { label: "Nuvem", value: "RiCloudLine" }, + { label: "Raio", value: "RiFlashlightFill" }, + { label: "Planta", value: "RiPlantLine" }, + { label: "Árvore", value: "RiSeedlingLine" }, + { label: "Globo", value: "RiGlobalLine" }, + { label: "Localização", value: "RiMapPin2Line" }, + { label: "Bússola", value: "RiCompassLine" }, + { label: "Reciclagem", value: "RiRecycleLine" }, + { label: "Cadeado", value: "RiLockLine" }, + { label: "Chave", value: "RiKeyLine" }, + { label: "Configurações", value: "RiSettings3Line" }, + { label: "Link", value: "RiLinkLine" }, + { label: "Anexo", value: "RiAttachmentLine" }, + { label: "Download", value: "RiDownloadLine" }, + { label: "Upload", value: "RiUploadLine" }, + { label: "Nuvem Download", value: "RiCloudDownloadLine" }, + { label: "Nuvem Upload", value: "RiCloudUploadLine" }, + ], + }, +]; + /** * Gets all available category icon options * @returns Array of icon options