Initial commit
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useTransition, useCallback, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
AcademicCapIcon,
|
||||
PlusIcon,
|
||||
PencilSquareIcon,
|
||||
TrashIcon,
|
||||
EyeIcon,
|
||||
DocumentDuplicateIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import ActionBtn from "@/app/(protected)/dashboard/components/ActionBtn";
|
||||
import Card from "@/app/(protected)/dashboard/components/Card";
|
||||
import SectionHeader from "@/app/(protected)/dashboard/components/SectionHeader";
|
||||
import Label from "@/app/(protected)/components/shared/Label";
|
||||
import { deleteExamTemplate, duplicateExamTemplate } from "@/app/lib/actions/examActions";
|
||||
|
||||
export default function TemplateList({
|
||||
templates: initialTemplates = [],
|
||||
setTemplates: setTemplatesProp
|
||||
}) {
|
||||
const [templates, setTemplates] = useState(initialTemplates);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [pendingDeleteId, setPendingDeleteId] = useState(null);
|
||||
|
||||
// Sync with parent prop when it changes
|
||||
useEffect(() => {
|
||||
setTemplates(initialTemplates);
|
||||
}, [initialTemplates]);
|
||||
|
||||
// Wrapper to update both local and parent state
|
||||
const updateTemplates = useCallback((updater) => {
|
||||
setTemplates(updater);
|
||||
if (setTemplatesProp) {
|
||||
setTemplatesProp(typeof updater === 'function' ? updater(templates) : updater);
|
||||
}
|
||||
}, [setTemplatesProp, templates]);
|
||||
|
||||
const handleDelete = useCallback(async (templateId) => {
|
||||
if (!confirm("Tem certeza que deseja excluir este template?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set pending state for UI feedback
|
||||
setPendingDeleteId(templateId);
|
||||
|
||||
startTransition(() => {
|
||||
// Perform the delete operation
|
||||
deleteExamTemplate(templateId).then((result) => {
|
||||
if (result.success) {
|
||||
updateTemplates((prev) => prev.filter((t) => t._id !== templateId));
|
||||
} else {
|
||||
alert(result.error || "Erro ao excluir template");
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error("Erro ao excluir template:", error);
|
||||
alert("Erro ao excluir template");
|
||||
}).finally(() => {
|
||||
setPendingDeleteId(null);
|
||||
});
|
||||
});
|
||||
}, [updateTemplates]);
|
||||
|
||||
const handleDuplicate = useCallback(async (templateId) => {
|
||||
startTransition(() => {
|
||||
duplicateExamTemplate(templateId).then((result) => {
|
||||
if (result.success && result.data) {
|
||||
// Ensure the duplicated template has a unique _id
|
||||
const newTemplate = result.data;
|
||||
console.log('Duplicated template _id:', newTemplate._id);
|
||||
updateTemplates((prev) => {
|
||||
// Check if _id already exists
|
||||
if (prev.some(t => t._id === newTemplate._id)) {
|
||||
console.warn('Duplicate _id detected, forcing refresh');
|
||||
window.location.reload();
|
||||
return prev;
|
||||
}
|
||||
return [newTemplate, ...prev];
|
||||
});
|
||||
} else {
|
||||
alert(result.error || "Erro ao duplicar template");
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error("Erro ao duplicar template:", error);
|
||||
alert("Erro ao duplicar template");
|
||||
});
|
||||
});
|
||||
}, [updateTemplates]);
|
||||
|
||||
// Use templates for rendering
|
||||
const displayTemplates = templates;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<SectionHeader title="Meus Templates" icon={<AcademicCapIcon className="w-5 h-5" />} />
|
||||
<ActionBtn
|
||||
icon={<PlusIcon className="w-5 h-5" />}
|
||||
label="Novo Template"
|
||||
onClick={() => {
|
||||
// This will be handled by the parent component
|
||||
window.dispatchEvent(new CustomEvent('open-template-form', { detail: null }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{displayTemplates.length === 0 ? (
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Nenhum template criado ainda.
|
||||
</p>
|
||||
) : (
|
||||
displayTemplates.map((template) => (
|
||||
<div
|
||||
key={template._id}
|
||||
className={`hover-card-light p-4 border border-neutral-200 dark:border-neutral-800 rounded-lg ${(isPending || pendingDeleteId === template._id) ? 'opacity-70' : ''}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<h3 key={`${template._id}-title`} className="text-base font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{template.title}
|
||||
</h3>
|
||||
{template.isPublic && (
|
||||
<Label key={`${template._id}-public`} color="blue" size="sm">Público</Label>
|
||||
)}
|
||||
{template.category && (
|
||||
<Label key={`${template._id}-category`} color="gray" size="sm">{template.category}</Label>
|
||||
)}
|
||||
</div>
|
||||
{template.description && (
|
||||
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400 line-clamp-1">
|
||||
{template.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-2 flex items-center gap-4 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<span key={`${template._id}-questions`}>{template.questions?.length || 0} questões</span>
|
||||
<span key={`${template._id}-points`}>{template.totalPoints || 0} pontos</span>
|
||||
{template.timeLimit && <span key={`${template._id}-time`}>{template.timeLimit} min</span>}
|
||||
<span key={`${template._id}-usage`}>Usado {template.usageCount || 0}x</span>
|
||||
</div>
|
||||
{template.tags && template.tags.length > 0 && (
|
||||
<div className="mt-2 flex gap-1 flex-wrap">
|
||||
{template.tags.map((tag, idx) => (
|
||||
<span key={`${template._id}-tag-${idx}`} className="text-xs px-2 py-0.5 rounded-full bg-neutral-100 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 flex-shrink-0">
|
||||
<ActionBtn
|
||||
key={`${template._id}-duplicate`}
|
||||
icon={<DocumentDuplicateIcon className="w-4 h-4" />}
|
||||
label="Duplicar"
|
||||
onClick={() => handleDuplicate(template._id)}
|
||||
variant="secondary"
|
||||
small
|
||||
disabled={isPending || pendingDeleteId === template._id}
|
||||
/>
|
||||
<ActionBtn
|
||||
key={`${template._id}-edit`}
|
||||
icon={<PencilSquareIcon className="w-4 h-4" />}
|
||||
label="Editar"
|
||||
onClick={() => {
|
||||
// This will be handled by the parent component
|
||||
window.dispatchEvent(new CustomEvent('open-template-form', { detail: template }));
|
||||
}}
|
||||
variant="secondary"
|
||||
small
|
||||
/>
|
||||
<ActionBtn
|
||||
key={`${template._id}-delete`}
|
||||
icon={<TrashIcon className="w-4 h-4" />}
|
||||
label="Excluir"
|
||||
onClick={() => handleDelete(template._id)}
|
||||
variant="secondary"
|
||||
small
|
||||
disabled={isPending || pendingDeleteId === template._id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user