feat: replace native confirm() dialogs with custom delete confirmation dialogs
- Add Dialog component for delete confirmation in trainers page - Add Dialog component for delete confirmation in workouts page - Show entity name in confirmation dialog - Dark theme styling to match app design - Add loading state with spinner during deletion
This commit is contained in:
@@ -4,6 +4,14 @@ import { useEffect, useState } from "react";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Plus, Trash2, Edit, Loader2, Users, AlertCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
@@ -16,6 +24,8 @@ export default function TrainersPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [trainerToDelete, setTrainerToDelete] = useState<Trainer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTrainers();
|
||||
@@ -49,14 +59,19 @@ export default function TrainersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm("Are you sure you want to delete this trainer?")) return;
|
||||
const openDeleteDialog = (trainer: Trainer) => {
|
||||
setTrainerToDelete(trainer);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
setDeletingId(id);
|
||||
const handleDelete = async () => {
|
||||
if (!trainerToDelete) return;
|
||||
|
||||
setDeletingId(trainerToDelete.id);
|
||||
try {
|
||||
const { error } = await supabase.from("trainers").delete().eq("id", id);
|
||||
const { error } = await supabase.from("trainers").delete().eq("id", trainerToDelete.id);
|
||||
if (error) throw error;
|
||||
setTrainers(trainers.filter((t) => t.id !== id));
|
||||
setTrainers(trainers.filter((t) => t.id !== trainerToDelete.id));
|
||||
toast.success("Trainer deleted", {
|
||||
description: "The trainer has been removed successfully."
|
||||
});
|
||||
@@ -65,6 +80,8 @@ export default function TrainersPage() {
|
||||
toast.error("Failed to delete trainer");
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
setDeleteDialogOpen(false);
|
||||
setTrainerToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -151,7 +168,7 @@ export default function TrainersPage() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-neutral-400 hover:text-red-500"
|
||||
onClick={() => handleDelete(trainer.id)}
|
||||
onClick={() => openDeleteDialog(trainer)}
|
||||
disabled={deletingId === trainer.id}
|
||||
>
|
||||
{deletingId === trainer.id ? (
|
||||
@@ -167,6 +184,41 @@ export default function TrainersPage() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent className="bg-neutral-900 border-neutral-800 text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Trainer</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Are you sure you want to delete {trainerToDelete?.name}? This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
className="border-neutral-700 text-neutral-300 hover:bg-neutral-800"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
disabled={!!deletingId}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
{deletingId ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
"Delete"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,14 @@ import {
|
||||
import { Plus, Trash2, Edit, Loader2, Eye } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import type { Database } from "@/lib/supabase";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
type Workout = Database["public"]["Tables"]["workouts"]["Row"];
|
||||
|
||||
@@ -26,6 +34,8 @@ export default function WorkoutsPage() {
|
||||
const [workouts, setWorkouts] = useState<Workout[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [workoutToDelete, setWorkoutToDelete] = useState<Workout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorkouts();
|
||||
@@ -47,14 +57,19 @@ export default function WorkoutsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm("Are you sure you want to delete this workout?")) return;
|
||||
const openDeleteDialog = (workout: Workout) => {
|
||||
setWorkoutToDelete(workout);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
setDeletingId(id);
|
||||
const handleDelete = async () => {
|
||||
if (!workoutToDelete) return;
|
||||
|
||||
setDeletingId(workoutToDelete.id);
|
||||
try {
|
||||
const { error } = await supabase.from("workouts").delete().eq("id", id);
|
||||
const { error } = await supabase.from("workouts").delete().eq("id", workoutToDelete.id);
|
||||
if (error) throw error;
|
||||
setWorkouts(workouts.filter((w) => w.id !== id));
|
||||
setWorkouts(workouts.filter((w) => w.id !== workoutToDelete.id));
|
||||
toast.success("Workout deleted", {
|
||||
description: "The workout has been removed successfully."
|
||||
});
|
||||
@@ -63,6 +78,8 @@ export default function WorkoutsPage() {
|
||||
toast.error("Failed to delete workout");
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
setDeleteDialogOpen(false);
|
||||
setWorkoutToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -159,7 +176,7 @@ export default function WorkoutsPage() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-neutral-400 hover:text-red-500"
|
||||
onClick={() => handleDelete(workout.id)}
|
||||
onClick={() => openDeleteDialog(workout)}
|
||||
disabled={deletingId === workout.id}
|
||||
>
|
||||
{deletingId === workout.id ? (
|
||||
@@ -177,6 +194,41 @@ export default function WorkoutsPage() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent className="bg-neutral-900 border-neutral-800 text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Workout</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Are you sure you want to delete "{workoutToDelete?.title}"? This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
className="border-neutral-700 text-neutral-300 hover:bg-neutral-800"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
disabled={!!deletingId}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
{deletingId ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
"Delete"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user