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:
keyhan
2026-05-24 00:33:16 +03:30
parent 695e05f948
commit abbe821d91
15 changed files with 352 additions and 83 deletions
+22
View File
@@ -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,
};
}