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:
parent
0663d1ce6c
commit
35ba8ab26a
@ -61,8 +61,10 @@ export class BillingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ZodResponse({ description: "Refresh payment methods", type: PaymentMethodListDto })
|
||||
async refreshPaymentMethods(@Request() req: RequestWithUser): Promise<PaymentMethodList> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
export * from "./useBilling";
|
||||
export * from "./usePaymentRefresh";
|
||||
export * from "./useInvoicesFilter";
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -1,14 +1,15 @@
|
||||
"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 [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<T>({ items }: { items: T[] }) {
|
||||
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) => {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user