refactor: optimize codebase for React 19 compiler (v1.2.6)

React 19 compiler auto-optimizes memoization, making manual hooks unnecessary.

Changes:
- Remove ~60 useCallback/useMemo across 16 files
- Remove React.memo from nav-button and return-button
- Simplify hydration with useSyncExternalStore (privacy-provider)
- Add CHANGELOG.md for version tracking

No functional changes - internal optimization only.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-02-04 13:14:10 +00:00
parent a70a83dd9d
commit 757626c468
18 changed files with 571 additions and 617 deletions

62
CHANGELOG.md Normal file
View File

@@ -0,0 +1,62 @@
# Changelog
Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
## [1.2.6] - 2025-02-04
### Alterado
- Refatoração para otimização do React 19 compiler
- Removidos `useCallback` e `useMemo` desnecessários (~60 instâncias)
- Removidos `React.memo` wrappers desnecessários
- Simplificados padrões de hidratação com `useSyncExternalStore`
### Arquivos modificados
- `hooks/use-calculator-state.ts`
- `hooks/use-form-state.ts`
- `hooks/use-month-period.ts`
- `components/auth/signup-form.tsx`
- `components/contas/accounts-page.tsx`
- `components/contas/transfer-dialog.tsx`
- `components/lancamentos/table/lancamentos-filters.tsx`
- `components/sidebar/nav-main.tsx`
- `components/month-picker/nav-button.tsx`
- `components/month-picker/return-button.tsx`
- `components/privacy-provider.tsx`
- `components/dashboard/category-history-widget.tsx`
- `components/anotacoes/note-dialog.tsx`
- `components/categorias/category-dialog.tsx`
- `components/confirm-action-dialog.tsx`
- `components/orcamentos/budget-dialog.tsx`
## [1.2.5] - 2025-02-01
### Adicionado
- Widget de pagadores no dashboard
- Avatares atualizados para pagadores
## [1.2.4] - 2025-01-22
### Corrigido
- Preservar formatação nas anotações
- Layout do card de anotações
## [1.2.3] - 2025-01-22
### Adicionado
- Versão exibida na sidebar
- Documentação atualizada
## [1.2.2] - 2025-01-22
### Alterado
- Atualização de dependências
- Aplicada formatação no código

View File

@@ -3,7 +3,6 @@
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react"; import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
import { import {
type ReactNode, type ReactNode,
useCallback,
useEffect, useEffect,
useRef, useRef,
useState, useState,
@@ -121,24 +120,18 @@ export function NoteDialog({
const disableSubmit = isPending || onlySpaces || unchanged || invalidLen; const disableSubmit = isPending || onlySpaces || unchanged || invalidLen;
const handleOpenChange = useCallback( const handleOpenChange = (v: boolean) => {
(v: boolean) => { setDialogOpen(v);
setDialogOpen(v); if (!v) setErrorMessage(null);
if (!v) setErrorMessage(null); };
},
[setDialogOpen],
);
const handleKeyDown = useCallback( const handleKeyDown = (e: React.KeyboardEvent) => {
(e: React.KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === "Enter")
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") (e.currentTarget as HTMLFormElement).requestSubmit();
(e.currentTarget as HTMLFormElement).requestSubmit(); if (e.key === "Escape") handleOpenChange(false);
if (e.key === "Escape") handleOpenChange(false); };
},
[handleOpenChange],
);
const handleAddTask = useCallback(() => { const handleAddTask = () => {
const text = normalize(newTaskText); const text = normalize(newTaskText);
if (!text) return; if (!text) return;
@@ -151,97 +144,77 @@ export function NoteDialog({
updateField("tasks", [...(formState.tasks || []), newTask]); updateField("tasks", [...(formState.tasks || []), newTask]);
setNewTaskText(""); setNewTaskText("");
requestAnimationFrame(() => newTaskRef.current?.focus()); requestAnimationFrame(() => newTaskRef.current?.focus());
}, [newTaskText, formState.tasks, updateField]); };
const handleRemoveTask = useCallback( const handleRemoveTask = (taskId: string) => {
(taskId: string) => { updateField(
updateField( "tasks",
"tasks", (formState.tasks || []).filter((t) => t.id !== taskId),
(formState.tasks || []).filter((t) => t.id !== taskId), );
); };
},
[formState.tasks, updateField],
);
const handleToggleTask = useCallback( const handleToggleTask = (taskId: string) => {
(taskId: string) => { updateField(
updateField( "tasks",
"tasks", (formState.tasks || []).map((t) =>
(formState.tasks || []).map((t) => t.id === taskId ? { ...t, completed: !t.completed } : t,
t.id === taskId ? { ...t, completed: !t.completed } : t, ),
), );
); };
},
[formState.tasks, updateField],
);
const handleSubmit = useCallback( const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
(e: React.FormEvent<HTMLFormElement>) => { e.preventDefault();
e.preventDefault(); setErrorMessage(null);
setErrorMessage(null);
const payload = { const payload = {
title: normalize(formState.title), title: normalize(formState.title),
description: formState.description.trim(), description: formState.description.trim(),
type: formState.type, type: formState.type,
tasks: formState.tasks, tasks: formState.tasks,
}; };
if (onlySpaces || invalidLen) { if (onlySpaces || invalidLen) {
setErrorMessage("Preencha os campos respeitando os limites."); setErrorMessage("Preencha os campos respeitando os limites.");
titleRef.current?.focus(); titleRef.current?.focus();
return; return;
} }
if (mode === "update" && !note?.id) { if (mode === "update" && !note?.id) {
const msg = "Não foi possível identificar a anotação a ser editada."; const msg = "Não foi possível identificar a anotação a ser editada.";
setErrorMessage(msg); setErrorMessage(msg);
toast.error(msg); toast.error(msg);
return; return;
} }
if (unchanged) { if (unchanged) {
toast.info("Nada para atualizar."); toast.info("Nada para atualizar.");
return; return;
} }
startTransition(async () => { startTransition(async () => {
let result; let result;
if (mode === "create") { if (mode === "create") {
result = await createNoteAction(payload); result = await createNoteAction(payload);
} else { } else {
if (!note?.id) { if (!note?.id) {
const msg = "ID da anotação não encontrado."; const msg = "ID da anotação não encontrado.";
setErrorMessage(msg); setErrorMessage(msg);
toast.error(msg); toast.error(msg);
return;
}
result = await updateNoteAction({ id: note.id, ...payload });
}
if (result.success) {
toast.success(result.message);
setDialogOpen(false);
return; return;
} }
setErrorMessage(result.error); result = await updateNoteAction({ id: note.id, ...payload });
toast.error(result.error); }
titleRef.current?.focus();
}); if (result.success) {
}, toast.success(result.message);
[ setDialogOpen(false);
formState.title, return;
formState.description, }
formState.type, setErrorMessage(result.error);
formState.tasks, toast.error(result.error);
mode, titleRef.current?.focus();
note, });
setDialogOpen, };
onlySpaces,
unchanged,
invalidLen,
],
);
return ( return (
<Dialog open={dialogOpen} onOpenChange={handleOpenChange}> <Dialog open={dialogOpen} onOpenChange={handleOpenChange}>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { RiCheckLine, RiCloseLine, RiLoader4Line } from "@remixicon/react"; import { RiCheckLine, RiCloseLine, RiLoader4Line } from "@remixicon/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { type FormEvent, useMemo, useState } from "react"; import { type FormEvent, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
@@ -90,10 +90,7 @@ export function SignupForm({ className, ...props }: DivProps) {
const [loadingEmail, setLoadingEmail] = useState(false); const [loadingEmail, setLoadingEmail] = useState(false);
const [loadingGoogle, setLoadingGoogle] = useState(false); const [loadingGoogle, setLoadingGoogle] = useState(false);
const passwordValidation = useMemo( const passwordValidation = validatePassword(password);
() => validatePassword(password),
[password],
);
async function handleSubmit(e: FormEvent<HTMLFormElement>) { async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault(); e.preventDefault();

View File

@@ -1,12 +1,6 @@
"use client"; "use client";
import { import { useEffect, useState, useTransition } from "react";
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
createCategoryAction, createCategoryAction,
@@ -76,14 +70,10 @@ export function CategoryDialog({
onOpenChange, onOpenChange,
); );
const initialState = useMemo( const initialState = buildInitialValues({
() => category,
buildInitialValues({ defaultType,
category, });
defaultType,
}),
[category, defaultType],
);
// Use form state hook for form management // Use form state hook for form management
const { formState, updateField, setFormState } = const { formState, updateField, setFormState } =
@@ -95,7 +85,7 @@ export function CategoryDialog({
setFormState(initialState); setFormState(initialState);
setErrorMessage(null); setErrorMessage(null);
} }
}, [dialogOpen, initialState, setFormState]); }, [dialogOpen, setFormState, category, defaultType]);
// Clear error when dialog closes // Clear error when dialog closes
useEffect(() => { useEffect(() => {
@@ -104,46 +94,43 @@ export function CategoryDialog({
} }
}, [dialogOpen]); }, [dialogOpen]);
const handleSubmit = useCallback( const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
(event: React.FormEvent<HTMLFormElement>) => { event.preventDefault();
event.preventDefault(); setErrorMessage(null);
setErrorMessage(null);
if (mode === "update" && !category?.id) { if (mode === "update" && !category?.id) {
const message = "Categoria inválida."; const message = "Categoria inválida.";
setErrorMessage(message); setErrorMessage(message);
toast.error(message); toast.error(message);
return;
}
const payload = {
name: formState.name.trim(),
type: formState.type,
icon: formState.icon.trim(),
};
startTransition(async () => {
const result =
mode === "create"
? await createCategoryAction(payload)
: await updateCategoryAction({
id: category?.id ?? "",
...payload,
});
if (result.success) {
toast.success(result.message);
setDialogOpen(false);
setFormState(initialState);
return; return;
} }
const payload = { setErrorMessage(result.error);
name: formState.name.trim(), toast.error(result.error);
type: formState.type, });
icon: formState.icon.trim(), };
};
startTransition(async () => {
const result =
mode === "create"
? await createCategoryAction(payload)
: await updateCategoryAction({
id: category?.id ?? "",
...payload,
});
if (result.success) {
toast.success(result.message);
setDialogOpen(false);
setFormState(initialState);
return;
}
setErrorMessage(result.error);
toast.error(result.error);
});
},
[category?.id, formState, initialState, mode, setDialogOpen, setFormState],
);
const title = mode === "create" ? "Nova categoria" : "Editar categoria"; const title = mode === "create" ? "Nova categoria" : "Editar categoria";
const description = const description =

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import type { VariantProps } from "class-variance-authority"; import type { VariantProps } from "class-variance-authority";
import { useCallback, useMemo, useState, useTransition } from "react"; import { useState, useTransition } from "react";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -49,22 +49,16 @@ export function ConfirmActionDialog({
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const dialogOpen = open ?? internalOpen; const dialogOpen = open ?? internalOpen;
const setDialogOpen = useCallback( const setDialogOpen = (value: boolean) => {
(value: boolean) => { if (open === undefined) {
if (open === undefined) { setInternalOpen(value);
setInternalOpen(value); }
} onOpenChange?.(value);
onOpenChange?.(value); };
},
[onOpenChange, open],
);
const resolvedPendingLabel = useMemo( const resolvedPendingLabel = pendingLabel ?? confirmLabel;
() => pendingLabel ?? confirmLabel,
[pendingLabel, confirmLabel],
);
const handleConfirm = useCallback(() => { const handleConfirm = () => {
if (!onConfirm) { if (!onConfirm) {
setDialogOpen(false); setDialogOpen(false);
return; return;
@@ -78,7 +72,7 @@ export function ConfirmActionDialog({
// Mantém o diálogo aberto para que o chamador trate o erro. // Mantém o diálogo aberto para que o chamador trate o erro.
} }
}); });
}, [onConfirm, setDialogOpen]); };
return ( return (
<AlertDialog open={dialogOpen} onOpenChange={setDialogOpen}> <AlertDialog open={dialogOpen} onOpenChange={setDialogOpen}>

View File

@@ -3,7 +3,7 @@
import { RiAddCircleLine, RiBankLine } from "@remixicon/react"; import { RiAddCircleLine, RiBankLine } from "@remixicon/react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { deleteAccountAction } from "@/app/(dashboard)/contas/actions"; import { deleteAccountAction } from "@/app/(dashboard)/contas/actions";
import { ConfirmActionDialog } from "@/components/confirm-action-dialog"; import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
@@ -47,45 +47,43 @@ export function AccountsPage({
const hasAccounts = accounts.length > 0; const hasAccounts = accounts.length > 0;
const orderedAccounts = useMemo(() => { const orderedAccounts = [...accounts].sort((a, b) => {
return [...accounts].sort((a, b) => { // Coloca inativas no final
// Coloca inativas no final const aIsInactive = a.status?.toLowerCase() === "inativa";
const aIsInactive = a.status?.toLowerCase() === "inativa"; const bIsInactive = b.status?.toLowerCase() === "inativa";
const bIsInactive = b.status?.toLowerCase() === "inativa";
if (aIsInactive && !bIsInactive) return 1; if (aIsInactive && !bIsInactive) return 1;
if (!aIsInactive && bIsInactive) return -1; if (!aIsInactive && bIsInactive) return -1;
// Mesma ordem alfabética dentro de cada grupo // Mesma ordem alfabética dentro de cada grupo
return a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" }); return a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" });
}); });
}, [accounts]);
const handleEdit = useCallback((account: Account) => { const handleEdit = (account: Account) => {
setSelectedAccount(account); setSelectedAccount(account);
setEditOpen(true); setEditOpen(true);
}, []); };
const handleEditOpenChange = useCallback((open: boolean) => { const handleEditOpenChange = (open: boolean) => {
setEditOpen(open); setEditOpen(open);
if (!open) { if (!open) {
setSelectedAccount(null); setSelectedAccount(null);
} }
}, []); };
const handleRemoveRequest = useCallback((account: Account) => { const handleRemoveRequest = (account: Account) => {
setAccountToRemove(account); setAccountToRemove(account);
setRemoveOpen(true); setRemoveOpen(true);
}, []); };
const handleRemoveOpenChange = useCallback((open: boolean) => { const handleRemoveOpenChange = (open: boolean) => {
setRemoveOpen(open); setRemoveOpen(open);
if (!open) { if (!open) {
setAccountToRemove(null); setAccountToRemove(null);
} }
}, []); };
const handleRemoveConfirm = useCallback(async () => { const handleRemoveConfirm = async () => {
if (!accountToRemove) { if (!accountToRemove) {
return; return;
} }
@@ -99,19 +97,19 @@ export function AccountsPage({
toast.error(result.error); toast.error(result.error);
throw new Error(result.error); throw new Error(result.error);
}, [accountToRemove]); };
const handleTransferRequest = useCallback((account: Account) => { const handleTransferRequest = (account: Account) => {
setTransferFromAccount(account); setTransferFromAccount(account);
setTransferOpen(true); setTransferOpen(true);
}, []); };
const handleTransferOpenChange = useCallback((open: boolean) => { const handleTransferOpenChange = (open: boolean) => {
setTransferOpen(open); setTransferOpen(open);
if (!open) { if (!open) {
setTransferFromAccount(null); setTransferFromAccount(null);
} }
}, []); };
const removeTitle = accountToRemove const removeTitle = accountToRemove
? `Remover conta "${accountToRemove.name}"?` ? `Remover conta "${accountToRemove.name}"?`

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useMemo, useState, useTransition } from "react"; import { useState, useTransition } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { transferBetweenAccountsAction } from "@/app/(dashboard)/contas/actions"; import { transferBetweenAccountsAction } from "@/app/(dashboard)/contas/actions";
import type { AccountData } from "@/app/(dashboard)/contas/data"; import type { AccountData } from "@/app/(dashboard)/contas/data";
@@ -62,15 +62,13 @@ export function TransferDialog({
const [period, setPeriod] = useState(currentPeriod); const [period, setPeriod] = useState(currentPeriod);
// Available destination accounts (exclude source account) // Available destination accounts (exclude source account)
const availableAccounts = useMemo( const availableAccounts = accounts.filter(
() => accounts.filter((account) => account.id !== fromAccountId), (account) => account.id !== fromAccountId,
[accounts, fromAccountId],
); );
// Source account info // Source account info
const fromAccount = useMemo( const fromAccount = accounts.find(
() => accounts.find((account) => account.id === fromAccountId), (account) => account.id === fromAccountId,
[accounts, fromAccountId],
); );
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {

View File

@@ -4,7 +4,7 @@ import {
RiBarChartBoxLine, RiBarChartBoxLine,
RiCloseLine, RiCloseLine,
} from "@remixicon/react"; } from "@remixicon/react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
@@ -49,36 +49,37 @@ export function CategoryHistoryWidget({ data }: CategoryHistoryWidgetProps) {
const [selectedCategories, setSelectedCategories] = useState<string[]>([]); const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const isFirstRender = useRef(true);
// Load selected categories from sessionStorage on mount // Load from sessionStorage on mount and save on changes
useEffect(() => { useEffect(() => {
setIsClient(true); setIsClient(true);
const stored = sessionStorage.getItem(STORAGE_KEY_SELECTED); // Only load from storage on first render
if (stored) { if (isFirstRender.current) {
try { const stored = sessionStorage.getItem(STORAGE_KEY_SELECTED);
const parsed = JSON.parse(stored); if (stored) {
if (Array.isArray(parsed)) { try {
const validCategories = parsed.filter((id) => const parsed = JSON.parse(stored);
data.allCategories.some((cat) => cat.id === id), if (Array.isArray(parsed)) {
); const validCategories = parsed.filter((id) =>
setSelectedCategories(validCategories.slice(0, 5)); data.allCategories.some((cat) => cat.id === id),
);
setSelectedCategories(validCategories.slice(0, 5));
}
} catch (_e) {
// Invalid JSON, ignore
} }
} catch (_e) {
// Invalid JSON, ignore
} }
} isFirstRender.current = false;
}, [data.allCategories]); } else {
// Save to storage on subsequent changes
// Save to sessionStorage when selection changes
useEffect(() => {
if (isClient) {
sessionStorage.setItem( sessionStorage.setItem(
STORAGE_KEY_SELECTED, STORAGE_KEY_SELECTED,
JSON.stringify(selectedCategories), JSON.stringify(selectedCategories),
); );
} }
}, [selectedCategories, isClient]); }, [selectedCategories, data.allCategories]);
// Filter data to show only selected categories with vibrant colors // Filter data to show only selected categories with vibrant colors
const filteredCategories = useMemo(() => { const filteredCategories = useMemo(() => {

View File

@@ -6,14 +6,7 @@ import {
RiFilter3Line, RiFilter3Line,
} from "@remixicon/react"; } from "@remixicon/react";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { import { type ReactNode, useEffect, useState, useTransition } from "react";
type ReactNode,
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Command, Command,
@@ -147,29 +140,24 @@ export function LancamentosFilters({
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const getParamValue = useCallback( const getParamValue = (key: string) =>
(key: string) => searchParams.get(key) ?? FILTER_EMPTY_VALUE, searchParams.get(key) ?? FILTER_EMPTY_VALUE;
[searchParams],
);
const handleFilterChange = useCallback( const handleFilterChange = (key: string, value: string | null) => {
(key: string, value: string | null) => { const nextParams = new URLSearchParams(searchParams.toString());
const nextParams = new URLSearchParams(searchParams.toString());
if (value && value !== FILTER_EMPTY_VALUE) { if (value && value !== FILTER_EMPTY_VALUE) {
nextParams.set(key, value); nextParams.set(key, value);
} else { } else {
nextParams.delete(key); nextParams.delete(key);
} }
startTransition(() => { startTransition(() => {
router.replace(`${pathname}?${nextParams.toString()}`, { router.replace(`${pathname}?${nextParams.toString()}`, {
scroll: false, scroll: false,
});
}); });
}, });
[pathname, router, searchParams], };
);
const [searchValue, setSearchValue] = useState(searchParams.get("q") ?? ""); const [searchValue, setSearchValue] = useState(searchParams.get("q") ?? "");
const currentSearchParam = searchParams.get("q") ?? ""; const currentSearchParam = searchParams.get("q") ?? "";
@@ -191,7 +179,7 @@ export function LancamentosFilters({
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [searchValue, currentSearchParam, handleFilterChange]); }, [searchValue, currentSearchParam, handleFilterChange]);
const handleReset = useCallback(() => { const handleReset = () => {
const periodValue = searchParams.get("periodo"); const periodValue = searchParams.get("periodo");
const nextParams = new URLSearchParams(); const nextParams = new URLSearchParams();
if (periodValue) { if (periodValue) {
@@ -205,41 +193,29 @@ export function LancamentosFilters({
: pathname; : pathname;
router.replace(target, { scroll: false }); router.replace(target, { scroll: false });
}); });
}, [pathname, router, searchParams]); };
const pagadorSelectOptions = useMemo( const pagadorSelectOptions = pagadorOptions.map((option) => ({
() => value: option.slug,
pagadorOptions.map((option) => ({ label: option.label,
value: option.slug, avatarUrl: option.avatarUrl,
label: option.label, }));
avatarUrl: option.avatarUrl,
})),
[pagadorOptions],
);
const contaOptions = useMemo( const contaOptions = contaCartaoOptions
() => .filter((option) => option.kind === "conta")
contaCartaoOptions .map((option) => ({
.filter((option) => option.kind === "conta") value: option.slug,
.map((option) => ({ label: option.label,
value: option.slug, logo: option.logo,
label: option.label, }));
logo: option.logo,
})),
[contaCartaoOptions],
);
const cartaoOptions = useMemo( const cartaoOptions = contaCartaoOptions
() => .filter((option) => option.kind === "cartao")
contaCartaoOptions .map((option) => ({
.filter((option) => option.kind === "cartao") value: option.slug,
.map((option) => ({ label: option.label,
value: option.slug, logo: option.logo,
label: option.label, }));
logo: option.logo,
})),
[contaCartaoOptions],
);
const categoriaValue = getParamValue("categoria"); const categoriaValue = getParamValue("categoria");
const selectedCategoria = const selectedCategoria =
@@ -262,21 +238,18 @@ export function LancamentosFilters({
const [categoriaOpen, setCategoriaOpen] = useState(false); const [categoriaOpen, setCategoriaOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const hasActiveFilters = useMemo(() => { const hasActiveFilters =
return ( searchParams.get("transacao") ||
searchParams.get("transacao") || searchParams.get("condicao") ||
searchParams.get("condicao") || searchParams.get("pagamento") ||
searchParams.get("pagamento") || searchParams.get("pagador") ||
searchParams.get("pagador") || searchParams.get("categoria") ||
searchParams.get("categoria") || searchParams.get("contaCartao");
searchParams.get("contaCartao")
);
}, [searchParams]);
const handleResetFilters = useCallback(() => { const handleResetFilters = () => {
handleReset(); handleReset();
setDrawerOpen(false); setDrawerOpen(false);
}, [handleReset]); };
return ( return (
<div className={cn("flex flex-wrap items-center gap-2", className)}> <div className={cn("flex flex-wrap items-center gap-2", className)}>

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react"; import { RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react";
import React from "react";
interface NavigationButtonProps { interface NavigationButtonProps {
direction: "left" | "right"; direction: "left" | "right";
@@ -9,25 +8,23 @@ interface NavigationButtonProps {
onClick: () => void; onClick: () => void;
} }
const NavigationButton = React.memo( export default function NavigationButton({
({ direction, disabled, onClick }: NavigationButtonProps) => { direction,
const Icon = direction === "left" ? RiArrowLeftSLine : RiArrowRightSLine; disabled,
onClick,
}: NavigationButtonProps) {
const Icon = direction === "left" ? RiArrowLeftSLine : RiArrowRightSLine;
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="text-month-picker-foreground transition-all duration-200 cursor-pointer rounded-lg p-1 hover:bg-month-picker-foreground/10 focus:outline-hidden focus:ring-2 focus:ring-month-picker-foreground/30 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent" className="text-month-picker-foreground transition-all duration-200 cursor-pointer rounded-lg p-1 hover:bg-month-picker-foreground/10 focus:outline-hidden focus:ring-2 focus:ring-month-picker-foreground/30 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent"
disabled={disabled} disabled={disabled}
aria-label={`Navegar para o mês ${ aria-label={`Navegar para o mês ${
direction === "left" ? "anterior" : "seguinte" direction === "left" ? "anterior" : "seguinte"
}`} }`}
> >
<Icon className="text-primary" size={18} /> <Icon className="text-primary" size={18} />
</button> </button>
); );
}, }
);
NavigationButton.displayName = "NavigationButton";
export default NavigationButton;

View File

@@ -1,6 +1,5 @@
"use client"; "use client";
import React from "react";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
interface ReturnButtonProps { interface ReturnButtonProps {
@@ -8,7 +7,7 @@ interface ReturnButtonProps {
onClick: () => void; onClick: () => void;
} }
const ReturnButton = React.memo(({ disabled, onClick }: ReturnButtonProps) => { export default function ReturnButton({ disabled, onClick }: ReturnButtonProps) {
return ( return (
<Button <Button
className="w-32 h-6 rounded-sm lowercase" className="w-32 h-6 rounded-sm lowercase"
@@ -20,8 +19,4 @@ const ReturnButton = React.memo(({ disabled, onClick }: ReturnButtonProps) => {
Ir para Mês Atual Ir para Mês Atual
</Button> </Button>
); );
}); }
ReturnButton.displayName = "ReturnButton";
export default ReturnButton;

View File

@@ -1,12 +1,6 @@
"use client"; "use client";
import { import { useEffect, useState, useTransition } from "react";
useCallback,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
createBudgetAction, createBudgetAction,
@@ -79,14 +73,10 @@ export function BudgetDialog({
onOpenChange, onOpenChange,
); );
const initialState = useMemo( const initialState = buildInitialValues({
() => budget,
buildInitialValues({ defaultPeriod,
budget, });
defaultPeriod,
}),
[budget, defaultPeriod],
);
// Use form state hook for form management // Use form state hook for form management
const { formState, updateField, setFormState } = const { formState, updateField, setFormState } =
@@ -98,7 +88,7 @@ export function BudgetDialog({
setFormState(initialState); setFormState(initialState);
setErrorMessage(null); setErrorMessage(null);
} }
}, [dialogOpen, initialState, setFormState]); }, [dialogOpen, setFormState, budget, defaultPeriod]);
// Clear error when dialog closes // Clear error when dialog closes
useEffect(() => { useEffect(() => {
@@ -107,67 +97,64 @@ export function BudgetDialog({
} }
}, [dialogOpen]); }, [dialogOpen]);
const handleSubmit = useCallback( const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
(event: React.FormEvent<HTMLFormElement>) => { event.preventDefault();
event.preventDefault(); setErrorMessage(null);
setErrorMessage(null);
if (mode === "update" && !budget?.id) { if (mode === "update" && !budget?.id) {
const message = "Orçamento inválido."; const message = "Orçamento inválido.";
setErrorMessage(message); setErrorMessage(message);
toast.error(message); toast.error(message);
return;
}
if (formState.categoriaId.length === 0) {
const message = "Selecione uma categoria.";
setErrorMessage(message);
toast.error(message);
return;
}
if (formState.period.length === 0) {
const message = "Informe o período.";
setErrorMessage(message);
toast.error(message);
return;
}
if (formState.amount.length === 0) {
const message = "Informe o valor limite.";
setErrorMessage(message);
toast.error(message);
return;
}
const payload = {
categoriaId: formState.categoriaId,
period: formState.period,
amount: formState.amount,
};
startTransition(async () => {
const result =
mode === "create"
? await createBudgetAction(payload)
: await updateBudgetAction({
id: budget?.id ?? "",
...payload,
});
if (result.success) {
toast.success(result.message);
setDialogOpen(false);
setFormState(initialState);
return; return;
} }
if (formState.categoriaId.length === 0) { setErrorMessage(result.error);
const message = "Selecione uma categoria."; toast.error(result.error);
setErrorMessage(message); });
toast.error(message); };
return;
}
if (formState.period.length === 0) {
const message = "Informe o período.";
setErrorMessage(message);
toast.error(message);
return;
}
if (formState.amount.length === 0) {
const message = "Informe o valor limite.";
setErrorMessage(message);
toast.error(message);
return;
}
const payload = {
categoriaId: formState.categoriaId,
period: formState.period,
amount: formState.amount,
};
startTransition(async () => {
const result =
mode === "create"
? await createBudgetAction(payload)
: await updateBudgetAction({
id: budget?.id ?? "",
...payload,
});
if (result.success) {
toast.success(result.message);
setDialogOpen(false);
setFormState(initialState);
return;
}
setErrorMessage(result.error);
toast.error(result.error);
});
},
[budget?.id, formState, initialState, mode, setDialogOpen, setFormState],
);
const title = mode === "create" ? "Novo orçamento" : "Editar orçamento"; const title = mode === "create" ? "Novo orçamento" : "Editar orçamento";
const description = const description =

View File

@@ -1,7 +1,14 @@
"use client"; "use client";
import type React from "react"; import type React from "react";
import { createContext, useContext, useEffect, useState } from "react"; import {
createContext,
useContext,
useEffect,
useRef,
useState,
useSyncExternalStore,
} from "react";
interface PrivacyContextType { interface PrivacyContextType {
privacyMode: boolean; privacyMode: boolean;
@@ -13,25 +20,41 @@ const PrivacyContext = createContext<PrivacyContextType | undefined>(undefined);
const STORAGE_KEY = "app:privacyMode"; const STORAGE_KEY = "app:privacyMode";
// Read from localStorage safely (returns false on server)
function getStoredValue(): boolean {
if (typeof window === "undefined") return false;
return localStorage.getItem(STORAGE_KEY) === "true";
}
// Subscribe to storage changes
function subscribeToStorage(callback: () => void) {
window.addEventListener("storage", callback);
return () => window.removeEventListener("storage", callback);
}
export function PrivacyProvider({ children }: { children: React.ReactNode }) { export function PrivacyProvider({ children }: { children: React.ReactNode }) {
const [privacyMode, setPrivacyMode] = useState(false); // useSyncExternalStore handles hydration safely
const [hydrated, setHydrated] = useState(false); const storedValue = useSyncExternalStore(
subscribeToStorage,
getStoredValue,
() => false, // Server snapshot
);
// Sincronizar com localStorage na montagem (evitar mismatch SSR/CSR) const [privacyMode, setPrivacyMode] = useState(storedValue);
useEffect(() => { const isFirstRender = useRef(true);
const stored = localStorage.getItem(STORAGE_KEY);
if (stored !== null) {
setPrivacyMode(stored === "true");
}
setHydrated(true);
}, []);
// Persistir mudanças no localStorage // Sync with stored value on mount
useEffect(() => { useEffect(() => {
if (hydrated) { if (isFirstRender.current) {
localStorage.setItem(STORAGE_KEY, String(privacyMode)); setPrivacyMode(storedValue);
isFirstRender.current = false;
} }
}, [privacyMode, hydrated]); }, [storedValue]);
// Persist to localStorage when privacyMode changes
useEffect(() => {
localStorage.setItem(STORAGE_KEY, String(privacyMode));
}, [privacyMode]);
const toggle = () => { const toggle = () => {
setPrivacyMode((prev) => !prev); setPrivacyMode((prev) => !prev);

View File

@@ -63,50 +63,45 @@ export function NavMain({ sections }: { sections: NavSection[] }) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const periodParam = searchParams.get(MONTH_PERIOD_PARAM); const periodParam = searchParams.get(MONTH_PERIOD_PARAM);
const isLinkActive = React.useCallback( const normalizedPathname =
(url: string) => { pathname.endsWith("/") && pathname !== "/"
const normalizedPathname = ? pathname.slice(0, -1)
pathname.endsWith("/") && pathname !== "/" : pathname;
? pathname.slice(0, -1)
: pathname;
const normalizedUrl =
url.endsWith("/") && url !== "/" ? url.slice(0, -1) : url;
// Verifica se é exatamente igual ou se o pathname começa com a URL const isLinkActive = (url: string) => {
return ( const normalizedUrl =
normalizedPathname === normalizedUrl || url.endsWith("/") && url !== "/" ? url.slice(0, -1) : url;
normalizedPathname.startsWith(`${normalizedUrl}/`)
);
},
[pathname],
);
const buildHrefWithPeriod = React.useCallback( // Verifica se é exatamente igual ou se o pathname começa com a URL
(url: string) => { return (
if (!periodParam) { normalizedPathname === normalizedUrl ||
return url; normalizedPathname.startsWith(`${normalizedUrl}/`)
} );
};
const [rawPathname, existingSearch = ""] = url.split("?"); const buildHrefWithPeriod = (url: string) => {
const normalizedPathname = if (!periodParam) {
rawPathname.endsWith("/") && rawPathname !== "/" return url;
? rawPathname.slice(0, -1) }
: rawPathname;
if (!PERIOD_AWARE_PATHS.has(normalizedPathname)) { const [rawPathname, existingSearch = ""] = url.split("?");
return url; const normalizedRawPathname =
} rawPathname.endsWith("/") && rawPathname !== "/"
? rawPathname.slice(0, -1)
: rawPathname;
const params = new URLSearchParams(existingSearch); if (!PERIOD_AWARE_PATHS.has(normalizedRawPathname)) {
params.set(MONTH_PERIOD_PARAM, periodParam); return url;
}
const queryString = params.toString(); const params = new URLSearchParams(existingSearch);
return queryString params.set(MONTH_PERIOD_PARAM, periodParam);
? `${normalizedPathname}?${queryString}`
: normalizedPathname; const queryString = params.toString();
}, return queryString
[periodParam], ? `${normalizedRawPathname}?${queryString}`
); : normalizedRawPathname;
};
const activeLinkClasses = const activeLinkClasses =
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-dark! hover:text-primary!"; "data-[active=true]:bg-sidebar-accent data-[active=true]:text-dark! hover:text-primary!";

View File

@@ -1,5 +1,5 @@
import type { VariantProps } from "class-variance-authority"; import type { VariantProps } from "class-variance-authority";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import type { buttonVariants } from "@/components/ui/button"; import type { buttonVariants } from "@/components/ui/button";
import { import {
formatLocaleValue, formatLocaleValue,
@@ -26,9 +26,9 @@ export function useCalculatorState() {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const resetCopiedTimeoutRef = useRef<number | undefined>(undefined); const resetCopiedTimeoutRef = useRef<number | undefined>(undefined);
const currentValue = useMemo(() => Number(display), [display]); const currentValue = Number(display);
const resultText = useMemo(() => { const resultText = (() => {
if (display === "Erro") { if (display === "Erro") {
return null; return null;
} }
@@ -39,49 +39,46 @@ export function useCalculatorState() {
} }
return formatLocaleValue(normalized); return formatLocaleValue(normalized);
}, [currentValue, display]); })();
const reset = useCallback(() => { const reset = () => {
setDisplay("0"); setDisplay("0");
setAccumulator(null); setAccumulator(null);
setOperator(null); setOperator(null);
setOverwrite(false); setOverwrite(false);
setHistory(null); setHistory(null);
}, []); };
const inputDigit = useCallback( const inputDigit = (digit: string) => {
(digit: string) => { // Check conditions before state updates
// Check conditions before state updates const shouldReset = overwrite || display === "Erro";
const shouldReset = overwrite || display === "Erro";
setDisplay((prev) => { setDisplay((prev) => {
if (shouldReset) {
return digit;
}
if (prev === "0") {
return digit;
}
// Limitar a 10 dígitos (excluindo sinal negativo e ponto decimal)
const digitCount = prev.replace(/[-.]/g, "").length;
if (digitCount >= 10) {
return prev;
}
return `${prev}${digit}`;
});
// Update related states after display update
if (shouldReset) { if (shouldReset) {
setOverwrite(false); return digit;
setHistory(null);
} }
},
[overwrite, display],
);
const inputDecimal = useCallback(() => { if (prev === "0") {
return digit;
}
// Limitar a 10 dígitos (excluindo sinal negativo e ponto decimal)
const digitCount = prev.replace(/[-.]/g, "").length;
if (digitCount >= 10) {
return prev;
}
return `${prev}${digit}`;
});
// Update related states after display update
if (shouldReset) {
setOverwrite(false);
setHistory(null);
}
};
const inputDecimal = () => {
// Check conditions before state updates // Check conditions before state updates
const shouldReset = overwrite || display === "Erro"; const shouldReset = overwrite || display === "Erro";
@@ -108,40 +105,37 @@ export function useCalculatorState() {
setOverwrite(false); setOverwrite(false);
setHistory(null); setHistory(null);
} }
}, [overwrite, display]); };
const setNextOperator = useCallback( const setNextOperator = (nextOperator: Operator) => {
(nextOperator: Operator) => { if (display === "Erro") {
if (display === "Erro") { reset();
reset(); return;
}
const value = currentValue;
if (accumulator === null || operator === null || overwrite) {
setAccumulator(value);
} else {
const result = performOperation(accumulator, value, operator);
const formatted = formatNumber(result);
setAccumulator(Number.isFinite(result) ? result : null);
setDisplay(formatted);
if (!Number.isFinite(result)) {
setOperator(null);
setOverwrite(true);
setHistory(null);
return; return;
} }
}
const value = currentValue; setOperator(nextOperator);
setOverwrite(true);
setHistory(null);
};
if (accumulator === null || operator === null || overwrite) { const evaluate = () => {
setAccumulator(value);
} else {
const result = performOperation(accumulator, value, operator);
const formatted = formatNumber(result);
setAccumulator(Number.isFinite(result) ? result : null);
setDisplay(formatted);
if (!Number.isFinite(result)) {
setOperator(null);
setOverwrite(true);
setHistory(null);
return;
}
}
setOperator(nextOperator);
setOverwrite(true);
setHistory(null);
},
[accumulator, currentValue, display, operator, overwrite, reset],
);
const evaluate = useCallback(() => {
if (operator === null || accumulator === null || display === "Erro") { if (operator === null || accumulator === null || display === "Erro") {
return; return;
} }
@@ -161,9 +155,9 @@ export function useCalculatorState() {
setOperator(null); setOperator(null);
setOverwrite(true); setOverwrite(true);
setHistory(operation); setHistory(operation);
}, [accumulator, currentValue, display, operator]); };
const toggleSign = useCallback(() => { const toggleSign = () => {
setDisplay((prev) => { setDisplay((prev) => {
if (prev === "Erro") { if (prev === "Erro") {
return prev; return prev;
@@ -177,9 +171,9 @@ export function useCalculatorState() {
setOverwrite(false); setOverwrite(false);
setHistory(null); setHistory(null);
} }
}, [overwrite]); };
const deleteLastDigit = useCallback(() => { const deleteLastDigit = () => {
setHistory(null); setHistory(null);
// Check conditions before state updates // Check conditions before state updates
@@ -209,9 +203,9 @@ export function useCalculatorState() {
} else if (overwrite) { } else if (overwrite) {
setOverwrite(false); setOverwrite(false);
} }
}, [overwrite, display]); };
const applyPercent = useCallback(() => { const applyPercent = () => {
setDisplay((prev) => { setDisplay((prev) => {
if (prev === "Erro") { if (prev === "Erro") {
return prev; return prev;
@@ -221,9 +215,9 @@ export function useCalculatorState() {
}); });
setOverwrite(true); setOverwrite(true);
setHistory(null); setHistory(null);
}, []); };
const expression = useMemo(() => { const expression = (() => {
if (display === "Erro") { if (display === "Erro") {
return "Erro"; return "Erro";
} }
@@ -240,68 +234,57 @@ export function useCalculatorState() {
} }
return formatLocaleValue(display); return formatLocaleValue(display);
}, [accumulator, display, operator, overwrite]); })();
const buttons = useMemo(() => { const makeOperatorHandler = (nextOperator: Operator) => () =>
const makeOperatorHandler = (nextOperator: Operator) => () => setNextOperator(nextOperator);
setNextOperator(nextOperator);
return [ const buttons: CalculatorButtonConfig[][] = [
[ [
{ label: "C", onClick: reset, variant: "destructive" }, { label: "C", onClick: reset, variant: "destructive" },
{ label: "⌫", onClick: deleteLastDigit, variant: "default" }, { label: "⌫", onClick: deleteLastDigit, variant: "default" },
{ label: "%", onClick: applyPercent, variant: "default" }, { label: "%", onClick: applyPercent, variant: "default" },
{ {
label: "÷", label: "÷",
onClick: makeOperatorHandler("divide"), onClick: makeOperatorHandler("divide"),
variant: "outline", variant: "outline",
}, },
], ],
[ [
{ label: "7", onClick: () => inputDigit("7") }, { label: "7", onClick: () => inputDigit("7") },
{ label: "8", onClick: () => inputDigit("8") }, { label: "8", onClick: () => inputDigit("8") },
{ label: "9", onClick: () => inputDigit("9") }, { label: "9", onClick: () => inputDigit("9") },
{ {
label: "×", label: "×",
onClick: makeOperatorHandler("multiply"), onClick: makeOperatorHandler("multiply"),
variant: "outline", variant: "outline",
}, },
], ],
[ [
{ label: "4", onClick: () => inputDigit("4") }, { label: "4", onClick: () => inputDigit("4") },
{ label: "5", onClick: () => inputDigit("5") }, { label: "5", onClick: () => inputDigit("5") },
{ label: "6", onClick: () => inputDigit("6") }, { label: "6", onClick: () => inputDigit("6") },
{ {
label: "-", label: "-",
onClick: makeOperatorHandler("subtract"), onClick: makeOperatorHandler("subtract"),
variant: "outline", variant: "outline",
}, },
], ],
[ [
{ label: "1", onClick: () => inputDigit("1") }, { label: "1", onClick: () => inputDigit("1") },
{ label: "2", onClick: () => inputDigit("2") }, { label: "2", onClick: () => inputDigit("2") },
{ label: "3", onClick: () => inputDigit("3") }, { label: "3", onClick: () => inputDigit("3") },
{ label: "+", onClick: makeOperatorHandler("add"), variant: "outline" }, { label: "+", onClick: makeOperatorHandler("add"), variant: "outline" },
], ],
[ [
{ label: "±", onClick: toggleSign, variant: "default" }, { label: "±", onClick: toggleSign, variant: "default" },
{ label: "0", onClick: () => inputDigit("0") }, { label: "0", onClick: () => inputDigit("0") },
{ label: ",", onClick: inputDecimal }, { label: ",", onClick: inputDecimal },
{ label: "=", onClick: evaluate, variant: "default" }, { label: "=", onClick: evaluate, variant: "default" },
], ],
] satisfies CalculatorButtonConfig[][]; ];
}, [
applyPercent,
deleteLastDigit,
evaluate,
inputDecimal,
inputDigit,
reset,
setNextOperator,
toggleSign,
]);
const copyToClipboard = useCallback(async () => { const copyToClipboard = async () => {
if (!resultText) return; if (!resultText) return;
try { try {
@@ -320,9 +303,9 @@ export function useCalculatorState() {
error, error,
); );
} }
}, [resultText]); };
const pasteFromClipboard = useCallback(async () => { const pasteFromClipboard = async () => {
if (!navigator.clipboard?.readText) return; if (!navigator.clipboard?.readText) return;
try { try {
@@ -364,7 +347,7 @@ export function useCalculatorState() {
} catch (error) { } catch (error) {
console.error("Não foi possível colar o valor na calculadora.", error); console.error("Não foi possível colar o valor na calculadora.", error);
} }
}, []); };
useEffect(() => { useEffect(() => {
return () => { return () => {

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from "react"; import { useState } from "react";
/** /**
* Hook for managing form state with type-safe field updates * Hook for managing form state with type-safe field updates
@@ -22,26 +22,23 @@ export function useFormState<T extends Record<string, any>>(initialValues: T) {
/** /**
* Updates a single field in the form state * Updates a single field in the form state
*/ */
const updateField = useCallback( const updateField = <K extends keyof T>(field: K, value: T[K]) => {
<K extends keyof T>(field: K, value: T[K]) => { setFormState((prev) => ({ ...prev, [field]: value }));
setFormState((prev) => ({ ...prev, [field]: value })); };
},
[],
);
/** /**
* Resets form to initial values * Resets form to initial values
*/ */
const resetForm = useCallback(() => { const resetForm = () => {
setFormState(initialValues); setFormState(initialValues);
}, [initialValues]); };
/** /**
* Updates multiple fields at once * Updates multiple fields at once
*/ */
const updateFields = useCallback((updates: Partial<T>) => { const updateFields = (updates: Partial<T>) => {
setFormState((prev) => ({ ...prev, ...updates })); setFormState((prev) => ({ ...prev, ...updates }));
}, []); };
return { return {
formState, formState,

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useMemo } from "react"; import { useMemo } from "react";
import { MONTH_NAMES } from "@/lib/utils/period"; import { MONTH_NAMES } from "@/lib/utils/period";
@@ -46,29 +46,23 @@ export function useMonthPeriod() {
}; };
}, [periodFromParams, defaultMonth, defaultYear, optionsMeses]); }, [periodFromParams, defaultMonth, defaultYear, optionsMeses]);
const buildHref = useCallback( const buildHref = (month: string, year: string | number) => {
(month: string, year: string | number) => { const normalizedMonth = normalizeMonth(month);
const normalizedMonth = normalizeMonth(month); const normalizedYear = String(year).trim();
const normalizedYear = String(year).trim();
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
params.set(PERIOD_PARAM, `${normalizedMonth}-${normalizedYear}`); params.set(PERIOD_PARAM, `${normalizedMonth}-${normalizedYear}`);
return `${pathname}?${params.toString()}`; return `${pathname}?${params.toString()}`;
}, };
[pathname, searchParams],
);
const replacePeriod = useCallback( const replacePeriod = (target: string) => {
(target: string) => { if (!target) {
if (!target) { return;
return; }
}
router.replace(target, { scroll: false }); router.replace(target, { scroll: false });
}, };
[router],
);
return { return {
monthNames: optionsMeses, monthNames: optionsMeses,

View File

@@ -1,6 +1,6 @@
{ {
"name": "opensheets", "name": "opensheets",
"version": "1.2.5", "version": "1.2.6",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",