Improve delete UX and prepaid credit time display.
Show minutes and local expiry for resource credits; add shared delete hook with row/card loading overlays, detail-page deleting modal, and disabled controls to prevent double-delete. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,3 +18,25 @@ export function parseMemoryToMi(mem: string): number {
|
||||
if (mem.endsWith('Ki')) return parseFloat(mem) / 1024;
|
||||
return parseFloat(mem);
|
||||
}
|
||||
|
||||
/** Human-readable time left (days, hours, minutes). */
|
||||
export function formatRemainingDurationMs(remainingMs: number): string {
|
||||
const ms = Math.max(0, remainingMs);
|
||||
const days = Math.floor(ms / 86400000);
|
||||
const hours = Math.floor((ms % 86400000) / 3600000);
|
||||
const minutes = Math.floor((ms % 3600000) / 60000);
|
||||
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
if (minutes > 0) return `${minutes}m`;
|
||||
return 'less than 1m';
|
||||
}
|
||||
|
||||
/** Expiry timestamp in the user's locale and timezone. */
|
||||
export function formatExpiresAtLocal(expiresAt: string | Date): string {
|
||||
const date = typeof expiresAt === 'string' ? new Date(expiresAt) : expiresAt;
|
||||
if (Number.isNaN(date.getTime())) return '—';
|
||||
return date.toLocaleString(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
type DeleteResponse = { resourceCredit?: unknown };
|
||||
|
||||
export function useApplicationDelete(options?: {
|
||||
invalidateKeys?: unknown[][];
|
||||
onSuccess?: (res: DeleteResponse, id: string) => void;
|
||||
onError?: () => void;
|
||||
successMessage?: string;
|
||||
successWithCreditMessage?: string;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (id: string) => api.delete(`/applications/${id}`).then((r) => r.data as DeleteResponse),
|
||||
onMutate: (id) => {
|
||||
setDeletingId(id);
|
||||
},
|
||||
onSettled: () => {
|
||||
setDeletingId(null);
|
||||
},
|
||||
onSuccess: (data, id) => {
|
||||
for (const key of options?.invalidateKeys ?? [['applications']]) {
|
||||
queryClient.invalidateQueries({ queryKey: key });
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['resource-credits'] });
|
||||
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess(data, id);
|
||||
} else if (data?.resourceCredit && options?.successWithCreditMessage) {
|
||||
toast.success(options.successWithCreditMessage);
|
||||
} else {
|
||||
toast.success(options?.successMessage ?? 'Deleted successfully');
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
if (options?.onError) options.onError();
|
||||
else toast.error('Failed to delete');
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteApplication: mutation.mutate,
|
||||
deletingId,
|
||||
isDeleting: (id: string) => mutation.isPending && deletingId === id,
|
||||
isAnyDeleting: mutation.isPending,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user