From 35ba8ab26a5579388a9844934179dffdc1b99719 Mon Sep 17 00:00:00 2001 From: barsa Date: Thu, 5 Mar 2026 16:46:45 +0900 Subject: [PATCH] refactor: optimize billing controller and enhance carousel functionality - Updated BillingController to use Promise.all for concurrent fetching of payment methods and WHMCS client ID, improving performance. - Modified package.json in the portal to enable turbopack for faster development builds. - Removed unused useInvoicesFilter hook from the billing feature, streamlining the codebase. - Enhanced useCarousel hook to support auto-play functionality and pause on user interaction, improving user experience in the landing page carousel. --- .../src/modules/billing/billing.controller.ts | 6 +- apps/portal/package.json | 2 +- .../src/features/billing/hooks/index.ts | 1 - .../billing/hooks/useInvoicesFilter.ts | 102 ------------------ .../landing-page/hooks/useInfiniteCarousel.ts | 73 ++++++++++--- 5 files changed, 65 insertions(+), 119 deletions(-) delete mode 100644 apps/portal/src/features/billing/hooks/useInvoicesFilter.ts diff --git a/apps/bff/src/modules/billing/billing.controller.ts b/apps/bff/src/modules/billing/billing.controller.ts index 633f8007..68cbbcac 100644 --- a/apps/bff/src/modules/billing/billing.controller.ts +++ b/apps/bff/src/modules/billing/billing.controller.ts @@ -61,8 +61,10 @@ export class BillingController { @HttpCode(HttpStatus.OK) @ZodResponse({ description: "Refresh payment methods", type: PaymentMethodListDto }) async refreshPaymentMethods(@Request() req: RequestWithUser): Promise { - await this.paymentService.invalidatePaymentMethodsCache(req.user.id); - const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(req.user.id); + const [, whmcsClientId] = await Promise.all([ + this.paymentService.invalidatePaymentMethodsCache(req.user.id), + this.mappingsService.getWhmcsClientIdOrThrow(req.user.id), + ]); return this.paymentService.getPaymentMethods(whmcsClientId, req.user.id); } diff --git a/apps/portal/package.json b/apps/portal/package.json index 73a0bb5f..d3985191 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "predev": "node ./scripts/dev-prep.mjs", - "dev": "next dev -p ${NEXT_PORT:-3000}", + "dev": "next dev --turbopack -p ${NEXT_PORT:-3000}", "build": "next build", "build:turbo": "next build", "build:analyze": "ANALYZE=true next build", diff --git a/apps/portal/src/features/billing/hooks/index.ts b/apps/portal/src/features/billing/hooks/index.ts index 849459f4..6ed24cf5 100644 --- a/apps/portal/src/features/billing/hooks/index.ts +++ b/apps/portal/src/features/billing/hooks/index.ts @@ -1,3 +1,2 @@ export * from "./useBilling"; export * from "./usePaymentRefresh"; -export * from "./useInvoicesFilter"; diff --git a/apps/portal/src/features/billing/hooks/useInvoicesFilter.ts b/apps/portal/src/features/billing/hooks/useInvoicesFilter.ts deleted file mode 100644 index 9385a7dd..00000000 --- a/apps/portal/src/features/billing/hooks/useInvoicesFilter.ts +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; - -import { useMemo, useState, useCallback } from "react"; -import type { Invoice, InvoiceStatus } from "@customer-portal/domain/billing"; - -export type InvoiceStatusFilter = InvoiceStatus | "all"; - -export interface InvoicesSummaryStats { - total: number; - paid: number; - unpaid: number; - overdue: number; -} - -export interface UseInvoicesFilterOptions { - invoices: Invoice[] | undefined; -} - -export interface UseInvoicesFilterResult { - // Filter state - searchTerm: string; - setSearchTerm: (value: string) => void; - statusFilter: InvoiceStatusFilter; - setStatusFilter: (value: InvoiceStatusFilter) => void; - - // Computed values - filteredInvoices: Invoice[]; - stats: InvoicesSummaryStats; - hasActiveFilters: boolean; - - // Actions - clearFilters: () => void; -} - -/** - * useInvoicesFilter - Hook for filtering and computing stats on invoices. - * - * Provides search, status filtering, summary statistics, and clear functionality - * for invoice list pages. - */ -export function useInvoicesFilter({ invoices }: UseInvoicesFilterOptions): UseInvoicesFilterResult { - const [searchTerm, setSearchTerm] = useState(""); - const [statusFilter, setStatusFilter] = useState("all"); - - const hasActiveFilters = searchTerm.trim() !== "" || statusFilter !== "all"; - - const clearFilters = useCallback(() => { - setSearchTerm(""); - setStatusFilter("all"); - }, []); - - const stats = useMemo(() => { - if (!invoices || invoices.length === 0) { - return { total: 0, paid: 0, unpaid: 0, overdue: 0 }; - } - - return invoices.reduce( - (acc, invoice) => { - acc.total++; - if (invoice.status === "Paid") { - acc.paid++; - } else if (invoice.status === "Unpaid") { - acc.unpaid++; - } else if (invoice.status === "Overdue") { - acc.overdue++; - } - return acc; - }, - { total: 0, paid: 0, unpaid: 0, overdue: 0 } - ); - }, [invoices]); - - const filteredInvoices = useMemo(() => { - if (!invoices) return []; - - return invoices.filter(invoice => { - // Search filter - match invoice number or description - if (searchTerm.trim()) { - const term = searchTerm.toLowerCase(); - const matchesNumber = invoice.number.toLowerCase().includes(term); - const matchesDescription = invoice.description?.toLowerCase().includes(term); - if (!matchesNumber && !matchesDescription) { - return false; - } - } - - // Status filter - return statusFilter === "all" || invoice.status === statusFilter; - }); - }, [invoices, searchTerm, statusFilter]); - - return { - searchTerm, - setSearchTerm, - statusFilter, - setStatusFilter, - filteredInvoices, - stats, - hasActiveFilters, - clearFilters, - }; -} diff --git a/apps/portal/src/features/landing-page/hooks/useInfiniteCarousel.ts b/apps/portal/src/features/landing-page/hooks/useInfiniteCarousel.ts index 4b60f4e8..286c5753 100644 --- a/apps/portal/src/features/landing-page/hooks/useInfiniteCarousel.ts +++ b/apps/portal/src/features/landing-page/hooks/useInfiniteCarousel.ts @@ -1,14 +1,15 @@ "use client"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; -export function useCarousel({ items }: { items: T[] }) { +export function useCarousel({ items, autoPlayMs = 5000 }: { items: T[]; autoPlayMs?: number }) { const total = items.length; const [activeIndex, setActiveIndex] = useState(0); const [direction, setDirection] = useState<"next" | "prev">("next"); const activeIndexRef = useRef(activeIndex); activeIndexRef.current = activeIndex; const touchXRef = useRef(0); + const pausedRef = useRef(false); const goTo = useCallback((i: number) => { setDirection(i > activeIndexRef.current ? "next" : "prev"); @@ -35,39 +36,85 @@ export function useCarousel({ items }: { items: T[] }) { if (touch) touchXRef.current = touch.clientX; }, []); - const onTouchEnd = useCallback( + // Auto-play: pause on user interaction, resume after delay + const interactionTimerRef = useRef | undefined>(undefined); + + const pauseAutoPlay = useCallback(() => { + pausedRef.current = true; + clearTimeout(interactionTimerRef.current); + interactionTimerRef.current = setTimeout(() => { + pausedRef.current = false; + }, autoPlayMs * 2); + }, [autoPlayMs]); + + const goToWithPause = useCallback( + (i: number) => { + pauseAutoPlay(); + goTo(i); + }, + [goTo, pauseAutoPlay] + ); + + const goNextWithPause = useCallback(() => { + pauseAutoPlay(); + goNext(); + }, [goNext, pauseAutoPlay]); + + const goPrevWithPause = useCallback(() => { + pauseAutoPlay(); + goPrev(); + }, [goPrev, pauseAutoPlay]); + + const onTouchEndWithPause = useCallback( (e: React.TouchEvent) => { const touch = e.changedTouches[0]; if (!touch) return; const diff = touchXRef.current - touch.clientX; if (Math.abs(diff) > 50) { + pauseAutoPlay(); if (diff > 0) goNext(); else goPrev(); } }, - [goNext, goPrev] + [goNext, goPrev, pauseAutoPlay] ); - const onKeyDown = useCallback( + const onKeyDownWithPause = useCallback( (e: React.KeyboardEvent) => { - if (e.key === "ArrowLeft") goPrev(); - else if (e.key === "ArrowRight") goNext(); + if (e.key === "ArrowLeft") { + pauseAutoPlay(); + goPrev(); + } else if (e.key === "ArrowRight") { + pauseAutoPlay(); + goNext(); + } }, - [goPrev, goNext] + [goPrev, goNext, pauseAutoPlay] ); + useEffect(() => { + if (total <= 1) return; + const id = setInterval(() => { + if (!pausedRef.current) { + setDirection("next"); + setActiveIndex(prev => (prev + 1) % total); + } + }, autoPlayMs); + return () => clearInterval(id); + }, [total, autoPlayMs]); + return { items, total, activeIndex, direction, - goTo, - goNext, - goPrev, + goTo: goToWithPause, + goNext: goNextWithPause, + goPrev: goPrevWithPause, reset, onTouchStart, - onTouchEnd, - onKeyDown, + onTouchEnd: onTouchEndWithPause, + onKeyDown: onKeyDownWithPause, }; }