This commit is contained in:
urieljarethbusiness-cpu
2026-05-19 14:21:56 -06:00
parent 4f3bc01dcb
commit 7361f7ced1
6 changed files with 133 additions and 11 deletions
+67 -10
View File
@@ -10,6 +10,8 @@ export default function DashboardPage() {
const [subjects, setSubjects] = useState<Subject[]>([]);
const [settings, setSettings] = useState<SettingsStatus | null>(null);
const [name, setName] = useState("");
const [editingSubject, setEditingSubject] = useState<Subject | null>(null);
const [editName, setEditName] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
@@ -31,9 +33,47 @@ export default function DashboardPage() {
async function createSubject(event: FormEvent) {
event.preventDefault();
if (!name.trim()) return;
await api.createSubject(name.trim());
setName("");
await load();
try {
setError("");
await api.createSubject(name.trim());
setName("");
await load();
} catch (err) {
setError(err instanceof Error ? err.message : "No se pudo crear la materia");
}
}
function startEdit(subject: Subject) {
setEditingSubject(subject);
setEditName(subject.name);
setError("");
}
async function saveEdit(event: FormEvent) {
event.preventDefault();
if (!editingSubject || !editName.trim()) return;
try {
setError("");
await api.updateSubject(editingSubject.id, editName.trim());
setEditingSubject(null);
setEditName("");
await load();
} catch (err) {
setError(err instanceof Error ? err.message : "No se pudo editar la materia");
}
}
async function deleteSubject(subject: Subject) {
const confirmed = window.confirm(`Eliminar la materia "${subject.name}" tambien borrara todas sus semanas, archivos subidos, Markdown procesado y exports. Esta accion no se puede deshacer.`);
if (!confirmed) return;
try {
setError("");
await api.deleteSubject(subject.id);
if (editingSubject?.id === subject.id) setEditingSubject(null);
await load();
} catch (err) {
setError(err instanceof Error ? err.message : "No se pudo eliminar la materia");
}
}
return (
@@ -50,13 +90,15 @@ export default function DashboardPage() {
{loading ? <Card>Cargando...</Card> : (
<div className="grid gap-4 md:grid-cols-2">
{subjects.map((subject) => (
<Link key={subject.id} href={`/subjects/${subject.id}`}>
<Card className="transition hover:border-blue-400/40 hover:bg-blue-500/10">
<div className="text-xl font-semibold">{subject.name}</div>
<div className="mt-2 text-sm text-slate-400">{subject.slug}</div>
<div className="mt-5 text-sm text-blue-300">Abrir materia</div>
</Card>
</Link>
<Card key={subject.id} className="transition hover:border-blue-400/40 hover:bg-blue-500/10">
<div className="text-xl font-semibold">{subject.name}</div>
<div className="mt-2 text-sm text-slate-400">{subject.slug}</div>
<div className="mt-5 flex flex-wrap gap-2 text-sm">
<Link className="rounded-lg bg-blue-600 px-3 py-2 font-medium text-white hover:bg-blue-500" href={`/subjects/${subject.id}`}>Abrir materia</Link>
<button className="rounded-lg border border-white/10 px-3 py-2 text-slate-200 hover:bg-white/10" type="button" onClick={() => startEdit(subject)}>Editar</button>
<button className="rounded-lg border border-red-400/30 px-3 py-2 text-red-200 hover:bg-red-500/10" type="button" onClick={() => deleteSubject(subject)}>Eliminar</button>
</div>
</Card>
))}
{subjects.length === 0 && <Card className="md:col-span-2">No hay materias todavia. Crea la primera para empezar.</Card>}
</div>
@@ -64,6 +106,21 @@ export default function DashboardPage() {
</section>
<aside className="space-y-4">
{editingSubject && (
<Card>
<h2 className="text-lg font-semibold">Editar materia</h2>
<p className="mt-1 text-sm text-slate-400">Solo cambia el nombre visible. La carpeta interna queda igual para mantener rutas estables.</p>
<form className="mt-4 space-y-3" onSubmit={saveEdit}>
<Label>Nombre</Label>
<Input value={editName} onChange={(event) => setEditName(event.target.value)} />
<div className="flex gap-2">
<Button className="flex-1" disabled={!editName.trim()}>Guardar</Button>
<button className="flex-1 rounded-xl border border-white/10 px-4 py-2 hover:bg-white/10" type="button" onClick={() => setEditingSubject(null)}>Cancelar</button>
</div>
</form>
</Card>
)}
<Card>
<h2 className="text-lg font-semibold">Nueva materia</h2>
<form className="mt-4 space-y-3" onSubmit={createSubject}>
@@ -62,6 +62,19 @@ export default function SubjectPage({ params }: { params: { subjectId: string }
}
}
async function deleteWeek(week: Week) {
const confirmed = window.confirm(`Eliminar la semana ${week.number} tambien borrara sus archivos subidos, Markdown procesado y exports. Esta accion no se puede deshacer.`);
if (!confirmed) return;
try {
setError("");
await api.deleteWeek(subjectId, week.number);
if (editingWeek?.id === week.id) setEditingWeek(null);
await load();
} catch (err) {
setError(err instanceof Error ? err.message : "No se pudo eliminar la semana");
}
}
return (
<Shell>
<Link href="/" className="text-sm text-blue-300">Volver a materias</Link>
@@ -78,6 +91,7 @@ export default function SubjectPage({ params }: { params: { subjectId: string }
<div className="mt-5 flex flex-wrap gap-2 text-sm">
<Link className="rounded-lg bg-blue-600 px-3 py-2 font-medium text-white hover:bg-blue-500" href={`/subjects/${subjectId}/weeks/${week.number}`}>Abrir semana</Link>
<button className="rounded-lg border border-white/10 px-3 py-2 text-slate-200 hover:bg-white/10" type="button" onClick={() => startEdit(week)}>Editar</button>
<button className="rounded-lg border border-red-400/30 px-3 py-2 text-red-200 hover:bg-red-500/10" type="button" onClick={() => deleteWeek(week)}>Eliminar</button>
</div>
</Card>
))}
+13
View File
@@ -65,6 +65,16 @@ export const api = {
body: JSON.stringify({ name }),
}));
},
async updateSubject(id: number, name: string) {
return parseResponse<Subject>(await fetch(`${API_URL}/subjects/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
}));
},
async deleteSubject(id: number) {
return parseResponse<{ status: string }>(await fetch(`${API_URL}/subjects/${id}`, { method: "DELETE" }));
},
async weeks(subjectId: number) {
return parseResponse<Week[]>(await fetch(`${API_URL}/subjects/${subjectId}/weeks`, { cache: "no-store" }));
},
@@ -85,6 +95,9 @@ export const api = {
body: JSON.stringify({ number, title: title || null }),
}));
},
async deleteWeek(subjectId: number, weekNumber: number) {
return parseResponse<{ status: string }>(await fetch(`${API_URL}/subjects/${subjectId}/weeks/${weekNumber}`, { method: "DELETE" }));
},
async sources(subjectId: number, weekNumber: number) {
return parseResponse<Source[]>(await fetch(`${API_URL}/subjects/${subjectId}/weeks/${weekNumber}/sources`, { cache: "no-store" }));
},