Initial commit

This commit is contained in:
Rafael Dias Martins
2026-08-31 14:10:20 -03:00
commit 8fcb5aac66
454 changed files with 60207 additions and 0 deletions
@@ -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>
);
}