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)
|
@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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./useBilling";
|
export * from "./useBilling";
|
||||||
export * from "./usePaymentRefresh";
|
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";
|
"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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user