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.
This commit is contained in:
barsa 2026-03-05 16:46:45 +09:00
parent 0663d1ce6c
commit 35ba8ab26a
5 changed files with 65 additions and 119 deletions

View File

@ -61,8 +61,10 @@ export class BillingController {
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodResponse({ description: "Refresh payment methods", type: PaymentMethodListDto }) @ZodResponse({ description: "Refresh payment methods", type: PaymentMethodListDto })
async refreshPaymentMethods(@Request() req: RequestWithUser): Promise<PaymentMethodList> { async refreshPaymentMethods(@Request() req: RequestWithUser): Promise<PaymentMethodList> {
await this.paymentService.invalidatePaymentMethodsCache(req.user.id); const [, whmcsClientId] = await Promise.all([
const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(req.user.id); this.paymentService.invalidatePaymentMethodsCache(req.user.id),
this.mappingsService.getWhmcsClientIdOrThrow(req.user.id),
]);
return this.paymentService.getPaymentMethods(whmcsClientId, req.user.id); return this.paymentService.getPaymentMethods(whmcsClientId, req.user.id);
} }

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"predev": "node ./scripts/dev-prep.mjs", "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": "next build",
"build:turbo": "next build", "build:turbo": "next build",
"build:analyze": "ANALYZE=true next build", "build:analyze": "ANALYZE=true next build",

View File

@ -1,3 +1,2 @@
export * from "./useBilling"; export * from "./useBilling";
export * from "./usePaymentRefresh"; export * from "./usePaymentRefresh";
export * from "./useInvoicesFilter";

View File

@ -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<InvoiceStatusFilter>("all");
const hasActiveFilters = searchTerm.trim() !== "" || statusFilter !== "all";
const clearFilters = useCallback(() => {
setSearchTerm("");
setStatusFilter("all");
}, []);
const stats = useMemo<InvoicesSummaryStats>(() => {
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<Invoice[]>(() => {
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,
};
}

View File

@ -1,14 +1,15 @@
"use client"; "use client";
import { useCallback, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
export function useCarousel<T>({ items }: { items: T[] }) { export function useCarousel<T>({ items, autoPlayMs = 5000 }: { items: T[]; autoPlayMs?: number }) {
const total = items.length; const total = items.length;
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const [direction, setDirection] = useState<"next" | "prev">("next"); const [direction, setDirection] = useState<"next" | "prev">("next");
const activeIndexRef = useRef(activeIndex); const activeIndexRef = useRef(activeIndex);
activeIndexRef.current = activeIndex; activeIndexRef.current = activeIndex;
const touchXRef = useRef(0); const touchXRef = useRef(0);
const pausedRef = useRef(false);
const goTo = useCallback((i: number) => { const goTo = useCallback((i: number) => {
setDirection(i > activeIndexRef.current ? "next" : "prev"); setDirection(i > activeIndexRef.current ? "next" : "prev");
@ -35,39 +36,85 @@ export function useCarousel<T>({ items }: { items: T[] }) {
if (touch) touchXRef.current = touch.clientX; if (touch) touchXRef.current = touch.clientX;
}, []); }, []);
const onTouchEnd = useCallback( // Auto-play: pause on user interaction, resume after delay
const interactionTimerRef = useRef<ReturnType<typeof setTimeout> | 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) => { (e: React.TouchEvent) => {
const touch = e.changedTouches[0]; const touch = e.changedTouches[0];
if (!touch) return; if (!touch) return;
const diff = touchXRef.current - touch.clientX; const diff = touchXRef.current - touch.clientX;
if (Math.abs(diff) > 50) { if (Math.abs(diff) > 50) {
pauseAutoPlay();
if (diff > 0) goNext(); if (diff > 0) goNext();
else goPrev(); else goPrev();
} }
}, },
[goNext, goPrev] [goNext, goPrev, pauseAutoPlay]
); );
const onKeyDown = useCallback( const onKeyDownWithPause = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
if (e.key === "ArrowLeft") goPrev(); if (e.key === "ArrowLeft") {
else if (e.key === "ArrowRight") goNext(); 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 { return {
items, items,
total, total,
activeIndex, activeIndex,
direction, direction,
goTo, goTo: goToWithPause,
goNext, goNext: goNextWithPause,
goPrev, goPrev: goPrevWithPause,
reset, reset,
onTouchStart, onTouchStart,
onTouchEnd, onTouchEnd: onTouchEndWithPause,
onKeyDown, onKeyDown: onKeyDownWithPause,
}; };
} }