diff --git a/CHANGELOG.md b/CHANGELOG.md index fdc2521..d29c360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR ### Alterado +- Períodos e navegação mensal: `useMonthPeriod` passou a usar os helpers centrais de período (`YYYY-MM`), o month-picker foi simplificado e o rótulo visual agora segue o formato `Março 2026`. +- Hooks e organização: hooks locais de calculadora, month-picker, logo picker e sidebar foram movidos para perto das respectivas features, deixando `/hooks` focado nos hooks realmente compartilhados. +- Estado de formulários e responsividade: `useFormState` ganhou APIs explícitas de reset/substituição no lugar do setter cru, e `useIsMobile` foi atualizado para assinatura estável com `useSyncExternalStore`, reduzindo a troca estrutural inicial no sidebar entre mobile e desktop. - Navegação e estrutura compartilhada: `components/navbar` e `components/sidebar` foram consolidados em `components/navigation/*`, componentes globais migraram para `components/shared/*` e os imports foram padronizados no projeto. - Dashboard e relatórios: a análise de parcelas foi movida para `/relatorios/analise-parcelas`, ações rápidas e widgets do dashboard foram refinados, e os cards de relatórios ganharam ajustes para evitar overflow no mobile. - Pré-lançamentos e lançamentos: tabs e cards da inbox ficaram mais consistentes no mobile, itens descartados podem voltar para `Pendente` e compras feitas no dia do fechamento do cartão agora entram na próxima fatura. diff --git a/components/anotacoes/note-dialog.tsx b/components/anotacoes/note-dialog.tsx index 10a8969..32ba333 100644 --- a/components/anotacoes/note-dialog.tsx +++ b/components/anotacoes/note-dialog.tsx @@ -85,17 +85,17 @@ export function NoteDialog({ const initialState = buildInitialValues(note); - const { formState, updateField, setFormState } = + const { formState, resetForm, updateField } = useFormState(initialState); useEffect(() => { if (dialogOpen) { - setFormState(buildInitialValues(note)); + resetForm(buildInitialValues(note)); setErrorMessage(null); setNewTaskText(""); requestAnimationFrame(() => titleRef.current?.focus()); } - }, [dialogOpen, note, setFormState]); + }, [dialogOpen, note, resetForm]); const dialogTitle = mode === "create" ? "Nova anotação" : "Editar anotação"; const description = diff --git a/components/calculadora/calculator-dialog.tsx b/components/calculadora/calculator-dialog.tsx index 0b7c7b5..4408997 100644 --- a/components/calculadora/calculator-dialog.tsx +++ b/components/calculadora/calculator-dialog.tsx @@ -16,8 +16,8 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { useDraggableDialog } from "@/hooks/use-draggable-dialog"; import { cn } from "@/lib/utils/ui"; +import { useDraggableDialog } from "./use-draggable-dialog"; type Variant = React.ComponentProps["variant"]; type Size = React.ComponentProps["size"]; diff --git a/components/calculadora/calculator-keypad.tsx b/components/calculadora/calculator-keypad.tsx index d17f159..2a3731b 100644 --- a/components/calculadora/calculator-keypad.tsx +++ b/components/calculadora/calculator-keypad.tsx @@ -1,5 +1,5 @@ +import type { CalculatorButtonConfig } from "@/components/calculadora/use-calculator-state"; import { Button } from "@/components/ui/button"; -import type { CalculatorButtonConfig } from "@/hooks/use-calculator-state"; import type { Operator } from "@/lib/utils/calculator"; import { cn } from "@/lib/utils/ui"; diff --git a/components/calculadora/calculator.tsx b/components/calculadora/calculator.tsx index ac0e4c5..95c8718 100644 --- a/components/calculadora/calculator.tsx +++ b/components/calculadora/calculator.tsx @@ -1,9 +1,9 @@ "use client"; import { CalculatorKeypad } from "@/components/calculadora/calculator-keypad"; +import { useCalculatorKeyboard } from "@/components/calculadora/use-calculator-keyboard"; +import { useCalculatorState } from "@/components/calculadora/use-calculator-state"; import { Button } from "@/components/ui/button"; -import { useCalculatorKeyboard } from "@/hooks/use-calculator-keyboard"; -import { useCalculatorState } from "@/hooks/use-calculator-state"; import { CalculatorDisplay } from "./calculator-display"; type CalculatorProps = { diff --git a/hooks/use-calculator-keyboard.ts b/components/calculadora/use-calculator-keyboard.ts similarity index 100% rename from hooks/use-calculator-keyboard.ts rename to components/calculadora/use-calculator-keyboard.ts diff --git a/hooks/use-calculator-state.ts b/components/calculadora/use-calculator-state.ts similarity index 100% rename from hooks/use-calculator-state.ts rename to components/calculadora/use-calculator-state.ts diff --git a/hooks/use-draggable-dialog.ts b/components/calculadora/use-draggable-dialog.ts similarity index 100% rename from hooks/use-draggable-dialog.ts rename to components/calculadora/use-draggable-dialog.ts diff --git a/components/cartoes/card-dialog.tsx b/components/cartoes/card-dialog.tsx index d5b2aa2..a6eed0f 100644 --- a/components/cartoes/card-dialog.tsx +++ b/components/cartoes/card-dialog.tsx @@ -13,6 +13,7 @@ import { updateCardAction, } from "@/app/(dashboard)/cartoes/actions"; import { LogoPickerDialog, LogoPickerTrigger } from "@/components/logo-picker"; +import { useLogoSelection } from "@/components/logo-picker/use-logo-selection"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -25,7 +26,6 @@ import { } from "@/components/ui/dialog"; import { useControlledState } from "@/hooks/use-controlled-state"; import { useFormState } from "@/hooks/use-form-state"; -import { useLogoSelection } from "@/hooks/use-logo-selection"; import { deriveNameFromLogo, normalizeLogo } from "@/lib/logo"; import { formatLimitInput } from "@/lib/utils/currency"; import { CardFormFields } from "./card-form-fields"; @@ -100,16 +100,16 @@ export function CardDialog({ ); // Use form state hook for form management - const { formState, updateField, updateFields, setFormState } = + const { formState, resetForm, updateField, updateFields } = useFormState(initialState); // Reset form when dialog opens useEffect(() => { if (dialogOpen) { - setFormState(initialState); + resetForm(initialState); setErrorMessage(null); } - }, [dialogOpen, initialState, setFormState]); + }, [dialogOpen, initialState, resetForm]); // Close logo dialog when main dialog closes useEffect(() => { @@ -173,7 +173,7 @@ export function CardDialog({ if (result.success) { toast.success(result.message); setDialogOpen(false); - setFormState(initialState); + resetForm(initialState); return; } @@ -181,7 +181,7 @@ export function CardDialog({ toast.error(result.error); }); }, - [card?.id, formState, initialState, mode, setDialogOpen, setFormState], + [card?.id, formState, initialState, mode, resetForm, setDialogOpen], ); const title = mode === "create" ? "Novo cartão" : "Editar cartão"; diff --git a/components/categorias/category-dialog.tsx b/components/categorias/category-dialog.tsx index 625379c..0f46aba 100644 --- a/components/categorias/category-dialog.tsx +++ b/components/categorias/category-dialog.tsx @@ -76,16 +76,16 @@ export function CategoryDialog({ }); // Use form state hook for form management - const { formState, updateField, setFormState } = + const { formState, resetForm, updateField } = useFormState(initialState); // Reset form when dialog opens useEffect(() => { if (dialogOpen) { - setFormState(initialState); + resetForm(initialState); setErrorMessage(null); } - }, [dialogOpen, setFormState, initialState]); + }, [dialogOpen, initialState, resetForm]); // Clear error when dialog closes useEffect(() => { @@ -123,7 +123,7 @@ export function CategoryDialog({ if (result.success) { toast.success(result.message); setDialogOpen(false); - setFormState(initialState); + resetForm(initialState); return; } diff --git a/components/contas/account-dialog.tsx b/components/contas/account-dialog.tsx index b5170eb..6feb93d 100644 --- a/components/contas/account-dialog.tsx +++ b/components/contas/account-dialog.tsx @@ -13,6 +13,7 @@ import { updateAccountAction, } from "@/app/(dashboard)/contas/actions"; import { LogoPickerDialog, LogoPickerTrigger } from "@/components/logo-picker"; +import { useLogoSelection } from "@/components/logo-picker/use-logo-selection"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -25,7 +26,6 @@ import { } from "@/components/ui/dialog"; import { useControlledState } from "@/hooks/use-controlled-state"; import { useFormState } from "@/hooks/use-form-state"; -import { useLogoSelection } from "@/hooks/use-logo-selection"; import { deriveNameFromLogo, normalizeLogo } from "@/lib/logo"; import { formatInitialBalanceInput } from "@/lib/utils/currency"; @@ -126,16 +126,16 @@ export function AccountDialog({ ); // Use form state hook for form management - const { formState, updateField, updateFields, setFormState } = + const { formState, resetForm, updateField, updateFields } = useFormState(initialState); // Reset form when dialog opens useEffect(() => { if (dialogOpen) { - setFormState(initialState); + resetForm(initialState); setErrorMessage(null); } - }, [dialogOpen, initialState, setFormState]); + }, [dialogOpen, initialState, resetForm]); // Close logo dialog when main dialog closes useEffect(() => { @@ -190,7 +190,7 @@ export function AccountDialog({ if (result.success) { toast.success(result.message); setDialogOpen(false); - setFormState(initialState); + resetForm(initialState); return; } @@ -198,7 +198,7 @@ export function AccountDialog({ toast.error(result.error); }); }, - [account?.id, formState, initialState, mode, setDialogOpen, setFormState], + [account?.id, formState, initialState, mode, resetForm, setDialogOpen], ); const title = mode === "create" ? "Nova conta" : "Editar conta"; diff --git a/components/lancamentos/dialogs/anticipate-installments-dialog/anticipate-installments-dialog.tsx b/components/lancamentos/dialogs/anticipate-installments-dialog/anticipate-installments-dialog.tsx index 0c5cd6c..bc4ed82 100644 --- a/components/lancamentos/dialogs/anticipate-installments-dialog/anticipate-installments-dialog.tsx +++ b/components/lancamentos/dialogs/anticipate-installments-dialog/anticipate-installments-dialog.tsx @@ -92,7 +92,7 @@ export function AnticipateInstallmentsDialog({ ); // Use form state hook for form management - const { formState, updateField, setFormState } = + const { formState, replaceForm, updateField } = useFormState({ anticipationPeriod: defaultPeriod, discount: "0", @@ -122,7 +122,7 @@ export function AnticipateInstallmentsDialog({ // Pré-preencher pagador e categoria da primeira parcela if (installments.length > 0) { const first = installments[0]; - setFormState({ + replaceForm({ anticipationPeriod: defaultPeriod, discount: "0", pagadorId: first.pagadorId ?? "", @@ -140,7 +140,7 @@ export function AnticipateInstallmentsDialog({ setIsLoadingInstallments(false); }); } - }, [dialogOpen, seriesId, defaultPeriod, setFormState]); + }, [defaultPeriod, dialogOpen, replaceForm, seriesId]); const totalAmount = useMemo(() => { return eligibleInstallments diff --git a/hooks/use-logo-selection.ts b/components/logo-picker/use-logo-selection.ts similarity index 95% rename from hooks/use-logo-selection.ts rename to components/logo-picker/use-logo-selection.ts index d711182..9603eb6 100644 --- a/hooks/use-logo-selection.ts +++ b/components/logo-picker/use-logo-selection.ts @@ -25,7 +25,7 @@ interface UseLogoSelectionProps { * mode: 'create', * currentLogo: formState.logo, * currentName: formState.name, - * onUpdate: (updates) => setFormState(prev => ({ ...prev, ...updates })) + * onUpdate: (updates) => updateFields(updates) * }); * ``` */ diff --git a/components/month-picker/month-navigation.tsx b/components/month-picker/month-navigation.tsx index 23ae542..1b352ab 100644 --- a/components/month-picker/month-navigation.tsx +++ b/components/month-picker/month-navigation.tsx @@ -3,61 +3,37 @@ import { useRouter } from "next/navigation"; import { useEffect, useMemo, useTransition } from "react"; import { Card } from "@/components/ui/card"; -import { useMonthPeriod } from "@/hooks/use-month-period"; +import { getNextPeriod, getPreviousPeriod } from "@/lib/utils/period"; import LoadingSpinner from "./loading-spinner"; import NavigationButton from "./nav-button"; import ReturnButton from "./return-button"; +import { useMonthPeriod } from "./use-month-period"; export default function MonthNavigation() { - const { - monthNames, - currentMonth, - currentYear, - defaultMonth, - defaultYear, - buildHref, - } = useMonthPeriod(); + const { period, currentMonth, currentYear, defaultPeriod, buildHref } = + useMonthPeriod(); const router = useRouter(); const [isPending, startTransition] = useTransition(); const currentMonthLabel = useMemo( - () => currentMonth.charAt(0).toUpperCase() + currentMonth.slice(1), - [currentMonth], + () => + `${currentMonth.charAt(0).toUpperCase()}${currentMonth.slice(1)} ${currentYear}`, + [currentMonth, currentYear], ); - - const currentMonthIndex = useMemo( - () => monthNames.indexOf(currentMonth), - [monthNames, currentMonth], + const prevTarget = useMemo( + () => buildHref(getPreviousPeriod(period)), + [buildHref, period], + ); + const nextTarget = useMemo( + () => buildHref(getNextPeriod(period)), + [buildHref, period], ); - - const prevTarget = useMemo(() => { - let idx = currentMonthIndex - 1; - let year = currentYear; - if (idx < 0) { - idx = monthNames.length - 1; - year = (parseInt(currentYear, 10) - 1).toString(); - } - return buildHref(monthNames[idx], year); - }, [currentMonthIndex, currentYear, monthNames, buildHref]); - - const nextTarget = useMemo(() => { - let idx = currentMonthIndex + 1; - let year = currentYear; - if (idx >= monthNames.length) { - idx = 0; - year = (parseInt(currentYear, 10) + 1).toString(); - } - return buildHref(monthNames[idx], year); - }, [currentMonthIndex, currentYear, monthNames, buildHref]); - const returnTarget = useMemo( - () => buildHref(defaultMonth, defaultYear), - [buildHref, defaultMonth, defaultYear], + () => buildHref(defaultPeriod), + [buildHref, defaultPeriod], ); - - const isDifferentFromCurrent = - currentMonth !== defaultMonth || currentYear !== defaultYear.toString(); + const isDifferentFromCurrent = period !== defaultPeriod; // Prefetch otimizado: apenas meses adjacentes (M-1, M+1) e mês atual // Isso melhora a performance da navegação sem sobrecarregar o cliente @@ -91,10 +67,9 @@ export default function MonthNavigation() {
{currentMonthLabel} - {currentYear}
{isPending && } diff --git a/components/month-picker/use-month-period.ts b/components/month-picker/use-month-period.ts new file mode 100644 index 0000000..50353d2 --- /dev/null +++ b/components/month-picker/use-month-period.ts @@ -0,0 +1,90 @@ +"use client"; + +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useMemo } from "react"; + +import { + formatPeriod, + formatPeriodForUrl, + formatPeriodParam, + MONTH_NAMES, + parsePeriodParam, +} from "@/lib/utils/period"; + +const PERIOD_PARAM = "periodo"; + +export function useMonthPeriod() { + const searchParams = useSearchParams(); + const pathname = usePathname(); + const router = useRouter(); + + const periodFromParams = searchParams.get(PERIOD_PARAM); + const referenceDate = useMemo(() => new Date(), []); + const defaultPeriod = useMemo( + () => + formatPeriod(referenceDate.getFullYear(), referenceDate.getMonth() + 1), + [referenceDate], + ); + const { period, monthName, year } = useMemo( + () => parsePeriodParam(periodFromParams, referenceDate), + [periodFromParams, referenceDate], + ); + const defaultMonth = useMemo( + () => MONTH_NAMES[referenceDate.getMonth()] ?? "", + [referenceDate], + ); + const defaultYear = useMemo( + () => referenceDate.getFullYear().toString(), + [referenceDate], + ); + + const buildHref = useCallback( + (targetPeriod: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set(PERIOD_PARAM, formatPeriodForUrl(targetPeriod)); + + return `${pathname}?${params.toString()}`; + }, + [pathname, searchParams], + ); + + const buildHrefFromMonth = useCallback( + (month: string, nextYear: string | number) => { + const parsedYear = Number.parseInt(String(nextYear).trim(), 10); + if (Number.isNaN(parsedYear)) { + return buildHref(defaultPeriod); + } + + const param = formatPeriodParam(month, parsedYear); + const parsed = parsePeriodParam(param, referenceDate); + return buildHref(parsed.period); + }, + [buildHref, defaultPeriod, referenceDate], + ); + + const replacePeriod = useCallback( + (targetPeriod: string) => { + if (!targetPeriod) { + return; + } + + router.replace(buildHref(targetPeriod), { scroll: false }); + }, + [buildHref, router], + ); + + return { + pathname, + period, + currentMonth: monthName, + currentYear: year.toString(), + defaultPeriod, + defaultMonth, + defaultYear, + buildHref, + buildHrefFromMonth, + replacePeriod, + }; +} + +export { PERIOD_PARAM as MONTH_PERIOD_PARAM }; diff --git a/components/orcamentos/budget-dialog.tsx b/components/orcamentos/budget-dialog.tsx index b29387e..7ab9d00 100644 --- a/components/orcamentos/budget-dialog.tsx +++ b/components/orcamentos/budget-dialog.tsx @@ -85,16 +85,16 @@ export function BudgetDialog({ }); // Use form state hook for form management - const { formState, updateField, setFormState } = + const { formState, resetForm, updateField } = useFormState(initialState); // Reset form when dialog opens useEffect(() => { if (dialogOpen) { - setFormState(initialState); + resetForm(initialState); setErrorMessage(null); } - }, [dialogOpen, setFormState, initialState]); + }, [dialogOpen, initialState, resetForm]); // Clear error when dialog closes useEffect(() => { @@ -153,7 +153,7 @@ export function BudgetDialog({ if (result.success) { toast.success(result.message); setDialogOpen(false); - setFormState(initialState); + resetForm(initialState); return; } diff --git a/components/pagadores/pagador-dialog.tsx b/components/pagadores/pagador-dialog.tsx index bde1b58..3b1a08e 100644 --- a/components/pagadores/pagador-dialog.tsx +++ b/components/pagadores/pagador-dialog.tsx @@ -95,7 +95,7 @@ export function PagadorDialog({ ); // Use form state hook for form management - const { formState, updateField, setFormState } = + const { formState, resetForm, updateField } = useFormState(initialState); const availableAvatars = useMemo(() => { @@ -111,10 +111,10 @@ export function PagadorDialog({ // Reset form when dialog opens useEffect(() => { if (dialogOpen) { - setFormState(initialState); + resetForm(initialState); setErrorMessage(null); } - }, [dialogOpen, initialState, setFormState]); + }, [dialogOpen, initialState, resetForm]); const handleSubmit = useCallback( (event: React.FormEvent) => { @@ -160,7 +160,7 @@ export function PagadorDialog({ if (result.success) { toast.success(result.message); setDialogOpen(false); - setFormState(initialState); + resetForm(initialState); return; } @@ -168,7 +168,7 @@ export function PagadorDialog({ toast.error(result.error); }); }, - [formState, initialState, mode, pagador?.id, setDialogOpen, setFormState], + [formState, initialState, mode, pagador?.id, resetForm, setDialogOpen], ); const title = mode === "create" ? "Novo pagador" : "Editar pagador"; diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx index f9fe79e..e8474a6 100644 --- a/components/ui/sidebar.tsx +++ b/components/ui/sidebar.tsx @@ -21,8 +21,8 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils/ui"; +import { useIsMobile } from "./use-mobile"; const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; @@ -162,7 +162,7 @@ function Sidebar({ variant?: "sidebar" | "floating" | "inset"; collapsible?: "offcanvas" | "icon" | "none"; }) { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + const { state, openMobile, setOpenMobile } = useSidebar(); if (collapsible === "none") { return ( @@ -179,14 +179,14 @@ function Sidebar({ ); } - if (isMobile) { - return ( + return ( + <> {children} - ); - } - return ( -
- {/* This is what handles the sidebar gap on desktop */}
- + ); } diff --git a/components/ui/use-mobile.ts b/components/ui/use-mobile.ts new file mode 100644 index 0000000..da96c73 --- /dev/null +++ b/components/ui/use-mobile.ts @@ -0,0 +1,26 @@ +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; +const MOBILE_MEDIA_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`; + +export function useIsMobile() { + const subscribe = React.useCallback((onStoreChange: () => void) => { + if (typeof window === "undefined") { + return () => {}; + } + + const mediaQueryList = window.matchMedia(MOBILE_MEDIA_QUERY); + mediaQueryList.addEventListener("change", onStoreChange); + return () => mediaQueryList.removeEventListener("change", onStoreChange); + }, []); + + const getSnapshot = React.useCallback(() => { + if (typeof window === "undefined") { + return false; + } + + return window.matchMedia(MOBILE_MEDIA_QUERY).matches; + }, []); + + return React.useSyncExternalStore(subscribe, getSnapshot, () => false); +} diff --git a/hooks/use-form-state.ts b/hooks/use-form-state.ts index fa63b48..def4fe1 100644 --- a/hooks/use-form-state.ts +++ b/hooks/use-form-state.ts @@ -1,10 +1,10 @@ -import { useState } from "react"; +import { useCallback, useRef, useState } from "react"; /** * Hook for managing form state with type-safe field updates * * @param initialValues - Initial form values - * @returns Object with formState, updateField, resetForm, setFormState + * @returns Object with formState, updateField, updateFields, replaceForm, resetForm * * @example * ```tsx @@ -16,37 +16,45 @@ import { useState } from "react"; * updateField('name', 'John'); * ``` */ -export function useFormState>( - initialValues: T, -) { +export function useFormState(initialValues: T) { + const latestInitialValuesRef = useRef(initialValues); + latestInitialValuesRef.current = initialValues; + const [formState, setFormState] = useState(initialValues); /** * Updates a single field in the form state */ - const updateField = (field: K, value: T[K]) => { - setFormState((prev) => ({ ...prev, [field]: value })); - }; + const updateField = useCallback( + (field: K, value: T[K]) => { + setFormState((prev) => ({ ...prev, [field]: value })); + }, + [], + ); /** * Resets form to initial values */ - const resetForm = () => { - setFormState(initialValues); - }; + const resetForm = useCallback((nextValues?: T) => { + setFormState(nextValues ?? latestInitialValuesRef.current); + }, []); /** * Updates multiple fields at once */ - const updateFields = (updates: Partial) => { + const updateFields = useCallback((updates: Partial) => { setFormState((prev) => ({ ...prev, ...updates })); - }; + }, []); + + const replaceForm = useCallback((nextValues: T) => { + setFormState(nextValues); + }, []); return { formState, updateField, updateFields, + replaceForm, resetForm, - setFormState, }; } diff --git a/hooks/use-mobile.ts b/hooks/use-mobile.ts deleted file mode 100644 index 0a89231..0000000 --- a/hooks/use-mobile.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; - -const MOBILE_BREAKPOINT = 768; - -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState( - undefined, - ); - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - }; - mql.addEventListener("change", onChange); - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - return () => mql.removeEventListener("change", onChange); - }, []); - - return !!isMobile; -} diff --git a/hooks/use-month-period.ts b/hooks/use-month-period.ts deleted file mode 100644 index 08d3d7a..0000000 --- a/hooks/use-month-period.ts +++ /dev/null @@ -1,79 +0,0 @@ -"use client"; - -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useMemo } from "react"; - -import { MONTH_NAMES } from "@/lib/utils/period"; - -const PERIOD_PARAM = "periodo"; - -const normalizeMonth = (value: string) => value.trim().toLowerCase(); - -export function useMonthPeriod() { - const searchParams = useSearchParams(); - const pathname = usePathname(); - const router = useRouter(); - - // Get current date info - const now = new Date(); - const currentYear = now.getFullYear(); - const currentMonthName = MONTH_NAMES[now.getMonth()]; - const optionsMeses = useMemo(() => [...MONTH_NAMES], []); - - const defaultMonth = currentMonthName; - const defaultYear = currentYear.toString(); - - const periodFromParams = searchParams.get(PERIOD_PARAM); - - const { month: currentMonth, year: currentYearValue } = useMemo(() => { - if (!periodFromParams) { - return { month: defaultMonth, year: defaultYear }; - } - - const [rawMonth, rawYear] = periodFromParams.split("-"); - const normalizedMonth = normalizeMonth(rawMonth ?? ""); - const normalizedYear = (rawYear ?? "").trim(); - const monthExists = optionsMeses.includes(normalizedMonth); - const parsedYear = Number.parseInt(normalizedYear, 10); - - if (!monthExists || Number.isNaN(parsedYear)) { - return { month: defaultMonth, year: defaultYear }; - } - - return { - month: normalizedMonth, - year: parsedYear.toString(), - }; - }, [periodFromParams, defaultMonth, defaultYear, optionsMeses]); - - const buildHref = (month: string, year: string | number) => { - const normalizedMonth = normalizeMonth(month); - const normalizedYear = String(year).trim(); - - const params = new URLSearchParams(searchParams.toString()); - params.set(PERIOD_PARAM, `${normalizedMonth}-${normalizedYear}`); - - return `${pathname}?${params.toString()}`; - }; - - const replacePeriod = (target: string) => { - if (!target) { - return; - } - - router.replace(target, { scroll: false }); - }; - - return { - monthNames: optionsMeses, - pathname, - currentMonth, - currentYear: currentYearValue, - defaultMonth, - defaultYear, - buildHref, - replacePeriod, - }; -} - -export { PERIOD_PARAM as MONTH_PERIOD_PARAM };