Refactor App component styles and remove highlights section
- Updated background gradient colors for a lighter theme. - Changed button styles to improve visibility. - Removed the highlights section to streamline the layout. - Adjusted text colors for better contrast and readability.
This commit is contained in:
@@ -229,39 +229,37 @@ function Page() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* NUEVO: botón de edición */}
|
||||
<div className="flex justify-end mb-2">
|
||||
<EditContenidosButton
|
||||
asignaturaId={a.id}
|
||||
value={a.contenidos as any}
|
||||
onSaved={(contenidos) => setA(s => ({ ...s, contenidos }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* …tu render flexible existente… */}
|
||||
{(() => {
|
||||
// --- helpers de normalización ---
|
||||
// helpers de normalización (como ya los tienes)
|
||||
const titleOf = (u: any): string => {
|
||||
// Si viene como arreglo [{titulo, seccion, subtemas}], u.temas tendrá pares [ 'titulo' , '…' ]
|
||||
const t = (u.temas || []).find(([k]: any[]) => String(k).toLowerCase() === "titulo")?.[1]
|
||||
if (typeof t === "string" && t.trim()) return t
|
||||
// Fallback: si la clave de la unidad es numérica, usa "Unidad N" o el título ya calculado
|
||||
return /^\s*\d+/.test(String(u.key))
|
||||
? (u.title && u.title !== u.key ? u.title : `Unidad ${u.key ? u.key : 1}`)
|
||||
: (u.title || String(u.key))
|
||||
}
|
||||
|
||||
const temasOf = (u: any): string[] => {
|
||||
const pairs: any[] = Array.isArray(u.temas) ? u.temas : []
|
||||
// 1) Estructura con subtemas
|
||||
const sub = pairs.find(([k]) => String(k).toLowerCase() === "subtemas")?.[1]
|
||||
if (Array.isArray(sub)) {
|
||||
// subtemas: ["t1", "t2", ...]
|
||||
return sub.map(String)
|
||||
}
|
||||
if (Array.isArray(sub)) return sub.map(String)
|
||||
if (sub && typeof sub === "object") {
|
||||
// subtemas: { "1": "t1", "2": "t2", ... }
|
||||
return Object.entries(sub)
|
||||
.sort(([a], [b]) => Number(a) - Number(b))
|
||||
.map(([, v]) => String(v))
|
||||
return Object.entries(sub).sort(([a], [b]) => Number(a) - Number(b)).map(([, v]) => String(v))
|
||||
}
|
||||
// 2) Estructura plana numerada { "1": "t1", "2": "t2", ... }
|
||||
const numerados = pairs
|
||||
.filter(([k, v]) => /^\d+$/.test(String(k)) && (typeof v === "string" || typeof v === "number"))
|
||||
.sort(([a], [b]) => Number(a) - Number(b))
|
||||
.map(([, v]) => String(v))
|
||||
if (numerados.length) return numerados
|
||||
// 3) Fallback: toma valores string excepto metadatos
|
||||
return pairs
|
||||
.filter(([k, v]) => !["titulo", "seccion"].includes(String(k).toLowerCase()) && typeof v === "string")
|
||||
.map(([, v]) => String(v))
|
||||
@@ -300,11 +298,8 @@ function Page() {
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
|
||||
{!visible.length && (
|
||||
<div className="text-sm text-neutral-500 py-6 text-center">
|
||||
No hay temas que coincidan.
|
||||
</div>
|
||||
<div className="text-sm text-neutral-500 py-6 text-center">No hay temas que coincidan.</div>
|
||||
)}
|
||||
</Accordion>
|
||||
)
|
||||
@@ -314,8 +309,16 @@ function Page() {
|
||||
|
||||
|
||||
{/* Bibliografía */}
|
||||
{a.bibliografia && a.bibliografia.length > 0 && (
|
||||
<Section id="bibliografia" title="Bibliografía" icon={Icons.LibraryBig}>
|
||||
<Section id="bibliografia" title="Bibliografía" icon={Icons.LibraryBig}>
|
||||
<div className="flex justify-end mb-2">
|
||||
<EditBibliografiaButton
|
||||
asignaturaId={a.id}
|
||||
value={a.bibliografia ?? []}
|
||||
onSaved={(bibliografia) => setA(s => ({ ...s, bibliografia }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{a.bibliografia && a.bibliografia.length > 0 ? (
|
||||
<ul className="space-y-2 text-sm text-neutral-800">
|
||||
{a.bibliografia.map((ref, i) => (
|
||||
<li key={i} className="flex items-start gap-2 leading-relaxed">
|
||||
@@ -324,8 +327,11 @@ function Page() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Section>
|
||||
)}
|
||||
) : (
|
||||
<div className="text-sm text-neutral-500">Sin bibliografía.</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
|
||||
{/* Evaluación */}
|
||||
{a.criterios_evaluacion && (
|
||||
@@ -557,3 +563,213 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function EditContenidosButton({
|
||||
asignaturaId,
|
||||
value,
|
||||
onSaved,
|
||||
}: {
|
||||
asignaturaId: string
|
||||
value: any
|
||||
onSaved: (contenidos: any) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
type UnitDraft = { title: string; temas: string[] }
|
||||
const [units, setUnits] = useState<UnitDraft[]>([])
|
||||
|
||||
// Normaliza el JSON (acepta estructuras flexibles)
|
||||
function normalize(v: any): UnitDraft[] {
|
||||
try {
|
||||
const entries = Object.entries(v ?? {})
|
||||
.sort(([a], [b]) => Number(a) - Number(b))
|
||||
.map(([key, val]) => {
|
||||
const obj = val as any
|
||||
// soporta: { titulo, subtemas:{ "1":"t1" } } | { "1":"t1" } | ["t1","t2"]
|
||||
const title = (typeof obj?.titulo === 'string' && obj.titulo.trim()) ? obj.titulo.trim() : `Unidad ${key}`
|
||||
let temas: string[] = []
|
||||
if (Array.isArray(obj?.subtemas)) temas = obj.subtemas.map(String)
|
||||
else if (obj?.subtemas && typeof obj.subtemas === 'object') {
|
||||
temas = Object.entries(obj.subtemas).sort(([a], [b]) => Number(a) - Number(b)).map(([, t]) => String(t))
|
||||
} else if (Array.isArray(obj)) {
|
||||
temas = obj.map(String)
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
const nums = Object.entries(obj).filter(([k, v]) => /^\d+$/.test(String(k)) && (typeof v === "string" || typeof v === "number"))
|
||||
if (nums.length) temas = nums.sort(([a], [b]) => Number(a) - Number(b)).map(([, t]) => String(t))
|
||||
}
|
||||
return { title, temas }
|
||||
})
|
||||
return entries.length ? entries : [{ title: 'Unidad 1', temas: [] }]
|
||||
} catch {
|
||||
return [{ title: 'Unidad 1', temas: [] }]
|
||||
}
|
||||
}
|
||||
|
||||
// Construye un JSON estable para guardar
|
||||
function buildPayload(us: UnitDraft[]) {
|
||||
const out: any = {}
|
||||
us.forEach((u, idx) => {
|
||||
const k = String(idx + 1)
|
||||
const sub: any = {}
|
||||
u.temas.filter(t => t.trim()).forEach((t, i) => { sub[String(i + 1)] = t.trim() })
|
||||
out[k] = { titulo: u.title.trim() || `Unidad ${k}`, subtemas: sub }
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
function openEditor() {
|
||||
setUnits(normalize(value))
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
setSaving(true)
|
||||
const contenidos = buildPayload(units)
|
||||
const { data, error } = await supabase
|
||||
.from('asignaturas')
|
||||
.update({ contenidos })
|
||||
.eq('id', asignaturaId)
|
||||
.select()
|
||||
.maybeSingle()
|
||||
setSaving(false)
|
||||
if (error) { alert(error.message || 'No se pudo guardar'); return }
|
||||
onSaved((data as any)?.contenidos ?? contenidos)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={openEditor}>
|
||||
<Icons.Pencil className="h-4 w-4 mr-2" /> Editar contenidos
|
||||
</Button>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar contenidos</DialogTitle>
|
||||
<DialogDescription>
|
||||
Títulos de unidad y temas (un tema por línea). Se guardará en un formato consistente con <code>titulo</code> y <code>subtemas</code>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 max-h-[60vh] overflow-auto pr-1">
|
||||
{units.map((u, i) => (
|
||||
<div key={i} className="rounded-2xl border p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="font-medium text-sm">Unidad {i + 1}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
title="Eliminar unidad"
|
||||
onClick={() => setUnits(prev => prev.filter((_, idx) => idx !== i))}
|
||||
>
|
||||
<Icons.Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label>Título</Label>
|
||||
<Input
|
||||
value={u.title}
|
||||
onChange={(e) => setUnits(prev => prev.map((uu, idx) => idx === i ? { ...uu, title: e.target.value } : uu))}
|
||||
placeholder={`Unidad ${i + 1}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Temas (uno por línea)</Label>
|
||||
<Textarea
|
||||
className="min-h-[120px]"
|
||||
value={u.temas.join('\n')}
|
||||
onChange={(e) => {
|
||||
const lines = e.target.value.split('\n')
|
||||
setUnits(prev => prev.map((uu, idx) => idx === i ? { ...uu, temas: lines } : uu))
|
||||
}}
|
||||
placeholder={`Tema 1\nTema 2\n…`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setUnits(prev => [...prev, { title: `Unidad ${prev.length + 1}`, temas: [] }])}
|
||||
>
|
||||
<Icons.Plus className="w-4 h-4 mr-2" /> Agregar unidad
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>Cancelar</Button>
|
||||
<Button onClick={save} disabled={saving}>{saving ? 'Guardando…' : 'Guardar'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function EditBibliografiaButton({
|
||||
asignaturaId,
|
||||
value,
|
||||
onSaved,
|
||||
}: {
|
||||
asignaturaId: string
|
||||
value: string[]
|
||||
onSaved: (refs: string[]) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [text, setText] = useState('')
|
||||
|
||||
function openEditor() {
|
||||
setText((value ?? []).join('\n'))
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
setSaving(true)
|
||||
const refs = text.split('\n').map(s => s.trim()).filter(Boolean)
|
||||
const { data, error } = await supabase
|
||||
.from('asignaturas')
|
||||
.update({ bibliografia: refs })
|
||||
.eq('id', asignaturaId)
|
||||
.select()
|
||||
.maybeSingle()
|
||||
setSaving(false)
|
||||
if (error) { alert(error.message || 'No se pudo guardar'); return }
|
||||
onSaved((data as any)?.bibliografia ?? refs)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={openEditor}>
|
||||
<Icons.Pencil className="h-4 w-4 mr-2" /> Editar bibliografía
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar bibliografía</DialogTitle>
|
||||
<DialogDescription>Escribe una referencia por línea.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
className="min-h-[260px]"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder={`Autor, Título, Editorial, Año\nDOI/URL\n…`}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>Cancelar</Button>
|
||||
<Button onClick={save} disabled={saving}>{saving ? 'Guardando…' : 'Guardar'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user