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
+18 -20
View File
@@ -1,14 +1,16 @@
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import Link from 'next/link';
import api from '@/lib/api';
import { toast } from 'react-toastify';
import type { Application } from '@/types';
import { managedServiceTypeLabel } from '@/lib/optional-service-defaults';
import { Database, Plus, AlertTriangle, Clock } from 'lucide-react';
import { useConfirm } from '@/components/confirm-modal';
import { DeleteButtonLabel } from '@/components/delete-button-label';
import { DeletingCardOverlay, deletingResourceMessage } from '@/components/deleting-overlay';
import { filterManagedServices } from '@/lib/product-type';
import { useApplicationDelete } from '@/lib/use-application-delete';
const lifecycleColors: Record<string, string> = {
active: 'text-green-600 bg-green-50',
@@ -50,7 +52,6 @@ function formatExpiry(expiresAt?: string): { text: string; urgent: boolean } {
}
export default function ServicesPage() {
const queryClient = useQueryClient();
const confirm = useConfirm();
const { data: servicesRaw = [], isLoading } = useQuery<Application[]>({
@@ -59,18 +60,10 @@ export default function ServicesPage() {
});
const services = filterManagedServices(servicesRaw);
const deleteMutation = useMutation({
mutationFn: (id: string) => api.delete(`/applications/${id}`),
onSuccess: (res) => {
queryClient.invalidateQueries({ queryKey: ['applications', 'managed'] });
queryClient.invalidateQueries({ queryKey: ['resource-credits'] });
if (res.data?.resourceCredit) {
toast.success('Service deleted. Prepaid resources are on your dashboard.');
} else {
toast.success('Service deleted');
}
},
onError: () => toast.error('Failed to delete service'),
const { deleteApplication, isDeleting, isAnyDeleting } = useApplicationDelete({
invalidateKeys: [['applications', 'managed']],
successMessage: 'Service deleted',
successWithCreditMessage: 'Service deleted. Prepaid resources are on your dashboard.',
});
if (isLoading) {
@@ -126,13 +119,17 @@ export default function ServicesPage() {
const lifecycle = svc.lifecycleStatus || 'active';
const expiry = formatExpiry(svc.planExpiresAt);
const latestStatus = svc.deployments?.[0]?.status || 'pending';
const cardDeleting = isDeleting(svc.id);
return (
<div
key={svc.id}
className={`card-hover flex flex-col sm:flex-row sm:items-center gap-4 ${
className={`relative card-hover flex flex-col sm:flex-row sm:items-center gap-4 ${
lifecycle === 'suspended' ? 'border-l-4 border-l-amber-400' : ''
} ${lifecycle === 'pending_deletion' ? 'border-l-4 border-l-red-400' : ''}`}
} ${lifecycle === 'pending_deletion' ? 'border-l-4 border-l-red-400' : ''} ${cardDeleting ? 'bg-gray-50' : ''}`}
>
{cardDeleting && (
<DeletingCardOverlay message={deletingResourceMessage('service', svc.name)} />
)}
<Link href={`/dashboard/services/${svc.id}`} className="flex items-center gap-3 flex-1 min-w-0 group">
<div className="w-10 h-10 rounded-lg bg-indigo-50 flex items-center justify-center shrink-0">
<Database className="w-5 h-5 text-indigo-600" />
@@ -161,7 +158,8 @@ export default function ServicesPage() {
</span>
<button
type="button"
className="btn-ghost text-sm text-red-600"
className="btn-ghost text-sm text-red-600 disabled:opacity-50 disabled:pointer-events-none"
disabled={isAnyDeleting}
onClick={async () => {
const ok = await confirm({
title: 'Delete service?',
@@ -169,10 +167,10 @@ export default function ServicesPage() {
confirmText: 'Delete',
variant: 'danger',
});
if (ok) deleteMutation.mutate(svc.id);
if (ok) deleteApplication(svc.id);
}}
>
Delete
<DeleteButtonLabel loading={isDeleting(svc.id)} />
</button>
</div>
</div>