mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-10-01 11:26:42 +02:00
feat: delete ai course (#8345)
* feat: delete ai course * Improve UI --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
This commit is contained in:
116
src/components/GenerateCourse/AICourseActions.tsx
Normal file
116
src/components/GenerateCourse/AICourseActions.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { MoreVertical, Play, Trash2 } from 'lucide-react';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||||
|
import { useKeydown } from '../../hooks/use-keydown';
|
||||||
|
import { useToast } from '../../hooks/use-toast';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { queryClient } from '../../stores/query-client';
|
||||||
|
import { httpDelete } from '../../lib/query-http';
|
||||||
|
|
||||||
|
type AICourseActionsType = {
|
||||||
|
courseSlug: string;
|
||||||
|
onDeleted?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AICourseActions(props: AICourseActionsType) {
|
||||||
|
const { courseSlug, onDeleted } = props;
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
|
|
||||||
|
const { mutate: deleteCourse, isPending: isDeleting } = useMutation(
|
||||||
|
{
|
||||||
|
mutationFn: async () => {
|
||||||
|
return httpDelete(`/v1-delete-ai-course/${courseSlug}`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Course deleted');
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
predicate: (query) => query.queryKey?.[0] === 'user-ai-courses',
|
||||||
|
});
|
||||||
|
onDeleted?.();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error?.message || 'Failed to delete course');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
queryClient,
|
||||||
|
);
|
||||||
|
|
||||||
|
useOutsideClick(dropdownRef, () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeydown('Escape', () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
className="h-full text-gray-400 hover:text-gray-700"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreVertical size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute right-0 top-8 z-10 w-48 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
|
||||||
|
<a
|
||||||
|
href={`/ai-tutor/${courseSlug}`}
|
||||||
|
className="flex w-full items-center gap-1.5 p-2 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
>
|
||||||
|
<Play className="h-3.5 w-3.5" />
|
||||||
|
Start Course
|
||||||
|
</a>
|
||||||
|
{!isConfirming && (
|
||||||
|
<button
|
||||||
|
className="flex w-full items-center gap-1.5 p-2 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
onClick={() => setIsConfirming(true)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
{!isDeleting ? (
|
||||||
|
<>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
Delete Course
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Deleting...'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isConfirming && (
|
||||||
|
<span className="flex w-full items-center justify-between gap-1.5 p-2 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-70">
|
||||||
|
Are you sure?
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsConfirming(false);
|
||||||
|
deleteCourse();
|
||||||
|
}}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="text-red-500 underline hover:text-red-800"
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsConfirming(false)}
|
||||||
|
className="text-red-500 underline hover:text-red-800"
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
import type { AICourseWithLessonCount } from '../../queries/ai-course';
|
import type { AICourseWithLessonCount } from '../../queries/ai-course';
|
||||||
import type { DifficultyLevel } from './AICourse';
|
import type { DifficultyLevel } from './AICourse';
|
||||||
import { BookOpen } from 'lucide-react';
|
import { BookOpen } from 'lucide-react';
|
||||||
|
import { AICourseActions } from './AICourseActions';
|
||||||
|
|
||||||
type AICourseCardProps = {
|
type AICourseCardProps = {
|
||||||
course: AICourseWithLessonCount;
|
course: AICourseWithLessonCount;
|
||||||
@@ -32,42 +33,50 @@ export function AICourseCard(props: AICourseCardProps) {
|
|||||||
totalTopics > 0 ? Math.round((completedTopics / totalTopics) * 100) : 0;
|
totalTopics > 0 ? Math.round((completedTopics / totalTopics) * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<div className="relative">
|
||||||
href={`/ai-tutor/${course.slug}`}
|
<a
|
||||||
className="hover:border-gray-3 00 group relative flex w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white p-4 text-left transition-all hover:bg-gray-50"
|
href={`/ai-tutor/${course.slug}`}
|
||||||
>
|
className="hover:border-gray-3 00 group relative flex w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white p-4 text-left transition-all hover:bg-gray-50"
|
||||||
<div className="flex items-center justify-between">
|
>
|
||||||
<span
|
<div className="flex items-center justify-between">
|
||||||
className={`rounded-full text-xs font-medium capitalize opacity-80 ${difficultyColor}`}
|
<span
|
||||||
>
|
className={`rounded-full text-xs font-medium capitalize opacity-80 ${difficultyColor}`}
|
||||||
{course.difficulty}
|
>
|
||||||
</span>
|
{course.difficulty}
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
<h3 className="my-2 text-base font-semibold text-gray-900">
|
|
||||||
{course.title}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="mt-auto flex items-center justify-between pt-2">
|
|
||||||
<div className="flex items-center text-xs text-gray-600">
|
|
||||||
<BookOpen className="mr-1 h-3.5 w-3.5" />
|
|
||||||
<span>{totalTopics} lessons</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{totalTopics > 0 && (
|
<h3 className="my-2 text-base font-semibold text-gray-900">
|
||||||
<div className="flex items-center">
|
{course.title}
|
||||||
<div className="mr-2 h-1.5 w-16 overflow-hidden rounded-full bg-gray-200">
|
</h3>
|
||||||
<div
|
|
||||||
className="h-full rounded-full bg-blue-600"
|
<div className="mt-auto flex items-center justify-between pt-2">
|
||||||
style={{ width: `${progressPercentage}%` }}
|
<div className="flex items-center text-xs text-gray-600">
|
||||||
/>
|
<BookOpen className="mr-1 h-3.5 w-3.5" />
|
||||||
</div>
|
<span>{totalTopics} lessons</span>
|
||||||
<span className="text-xs font-medium text-gray-700">
|
|
||||||
{progressPercentage}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
{totalTopics > 0 && (
|
||||||
</a>
|
<div className="flex items-center">
|
||||||
|
<div className="mr-2 h-1.5 w-16 overflow-hidden rounded-full bg-gray-200">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-blue-600"
|
||||||
|
style={{ width: `${progressPercentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-medium text-gray-700">
|
||||||
|
{progressPercentage}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{course.slug && (
|
||||||
|
<div className="absolute right-2 top-2">
|
||||||
|
<AICourseActions courseSlug={course.slug} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user