Que el renderizado no dependa de los query params #81

Merged
roberto.silva merged 4 commits from issue/80-deshacerse-de-todos-estos-query-params-de-la-url into main 2026-02-06 22:01:25 +00:00
3 changed files with 51 additions and 24 deletions
Showing only changes of commit 31a47934e5 - Show all commits

View File

@@ -76,31 +76,43 @@ function RouteComponent() {
mutate({ planId, patch }) mutate({ planId, patch })
} }
const MAX_CHARACTERS = 200
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
// 1. Permitir teclas de control (Borrar, flechas, etc.) siempre
const isControlKey =
e.key === 'Backspace' ||
e.key === 'Delete' ||
e.key.includes('Arrow') ||
e.metaKey ||
e.ctrlKey
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault() e.preventDefault()
e.currentTarget.blur() // Esto disparará el onBlur automáticamente e.currentTarget.blur()
return
}
// 2. Bloquear si excede los 200 caracteres y no es una tecla de control
const currentText = e.currentTarget.textContent || ''
if (currentText.length >= MAX_CHARACTERS && !isControlKey) {
e.preventDefault()
} }
} }
const handleBlurNombre = (e: React.FocusEvent<HTMLSpanElement>) => { const handlePaste = (e: React.ClipboardEvent<HTMLSpanElement>) => {
const nuevoNombre = e.currentTarget.textContent || '' e.preventDefault()
setNombrePlan(nuevoNombre) const text = e.clipboardData.getData('text/plain')
const currentText = e.currentTarget.textContent || ''
// Solo guardamos si el valor es realmente distinto al de la base de datos // Calcular cuánto espacio queda
if (nuevoNombre !== data?.nombre) { const remainingSpace = MAX_CHARACTERS - currentText.length
persistChange({ nombre: nuevoNombre })
}
}
const handleSelectNivel = (n: string) => { if (remainingSpace > 0) {
setNivelPlan(n) const slicedText = text.slice(0, remainingSpace)
// Guardamos inmediatamente al seleccionar document.execCommand('insertText', false, slicedText)
if (n !== data?.nivel) {
persistChange({ nivel: n })
} }
} }
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-white">
{/* 1. Header Superior */} {/* 1. Header Superior */}
@@ -127,8 +139,9 @@ function RouteComponent() {
) : ( ) : (
<div className="flex flex-col items-start justify-between gap-4 md:flex-row"> <div className="flex flex-col items-start justify-between gap-4 md:flex-row">
<div> <div>
<h1 className="flex items-baseline gap-2 text-3xl font-bold tracking-tight text-slate-900"> <h1 className="flex flex-wrap items-baseline gap-2 text-3xl leading-tight font-bold tracking-tight text-slate-900">
<span>{nivelPlan} en</span> {/* El prefijo "Nivel en" lo mantenemos simple */}
<span className="shrink-0">{nivelPlan} en</span>
<span <span
role="textbox" role="textbox"
tabIndex={0} tabIndex={0}
@@ -136,14 +149,17 @@ function RouteComponent() {
suppressContentEditableWarning suppressContentEditableWarning
spellCheck={false} spellCheck={false}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onPaste={handlePaste} // Añadido para controlar lo que pegan
onBlur={(e) => { onBlur={(e) => {
const nuevoNombre = e.currentTarget.textContent || '' const nuevoNombre =
e.currentTarget.textContent?.trim() || ''
setNombrePlan(nuevoNombre) setNombrePlan(nuevoNombre)
if (nuevoNombre !== data?.nombre) { if (nuevoNombre !== data?.nombre) {
mutate({ planId, patch: { nombre: nuevoNombre } }) mutate({ planId, patch: { nombre: nuevoNombre } })
} }
}} }}
className="cursor-text border-b border-transparent transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500" // Clases añadidas: break-words y whitespace-pre-wrap para el wrap
className="block w-full cursor-text border-b border-transparent break-words whitespace-pre-wrap transition-colors outline-none select-text hover:border-slate-300 focus:border-teal-500 sm:inline-block sm:w-auto"
style={{ textDecoration: 'none' }} style={{ textDecoration: 'none' }}
> >
{nombrePlan} {nombrePlan}

View File

@@ -93,7 +93,6 @@ function DatosGeneralesPage() {
requerido: true, requerido: true,
// 👇 TIPO DE CAMPO
tipo: Array.isArray(schema?.enum) tipo: Array.isArray(schema?.enum)
? 'select' ? 'select'
: schema?.type === 'number' : schema?.type === 'number'

View File

@@ -284,6 +284,16 @@ function MapaCurricularPage() {
const ciclosTotales = Number(ciclo) const ciclosTotales = Number(ciclo)
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1) const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1)
const [editingData, setEditingData] = useState<Asignatura | null>(null) const [editingData, setEditingData] = useState<Asignatura | null>(null)
const handleIntegerChange = (value: string) => {
if (value === '') return value
// Solo números, máximo 3 cifras
const regex = /^\d{1,3}$/
if (!regex.test(value)) return null
return value
}
const handleDecimalChange = (value: string, max?: number): string | null => { const handleDecimalChange = (value: string, max?: number): string | null => {
if (value === '') return '' if (value === '') return ''
@@ -562,7 +572,6 @@ function MapaCurricularPage() {
> >
{editingLineaId === linea.id ? ( {editingLineaId === linea.id ? (
<Input <Input
autoFocus
className="h-7 bg-white text-xs" className="h-7 bg-white text-xs"
value={tempNombreLinea} value={tempNombreLinea}
onChange={(e) => setTempNombreLinea(e.target.value)} onChange={(e) => setTempNombreLinea(e.target.value)}
@@ -572,6 +581,7 @@ function MapaCurricularPage() {
} }
/> />
) : ( ) : (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<span <span
className="cursor-pointer text-xs font-bold hover:underline" className="cursor-pointer text-xs font-bold hover:underline"
onClick={() => { onClick={() => {
@@ -727,6 +737,7 @@ function MapaCurricularPage() {
Clave Clave
</label> </label>
<Input <Input
maxLength={100}
value={editingData.clave} value={editingData.clave}
onChange={(e) => onChange={(e) =>
setEditingData({ ...editingData, clave: e.target.value }) setEditingData({ ...editingData, clave: e.target.value })
@@ -738,6 +749,7 @@ function MapaCurricularPage() {
Nombre Nombre
</label> </label>
<Input <Input
maxLength={200}
value={editingData.nombre} value={editingData.nombre}
onChange={(e) => onChange={(e) =>
setEditingData({ ...editingData, nombre: e.target.value }) setEditingData({ ...editingData, nombre: e.target.value })
@@ -775,7 +787,7 @@ function MapaCurricularPage() {
type="number" type="number"
value={editingData.hd} value={editingData.hd}
onChange={(e) => { onChange={(e) => {
const val = handleDecimalChange(e.target.value, 10) const val = handleIntegerChange(e.target.value)
if (val !== null) { if (val !== null) {
setEditingData({ setEditingData({
...editingData, ...editingData,
@@ -793,7 +805,7 @@ function MapaCurricularPage() {
type="number" type="number"
value={editingData.hi} value={editingData.hi}
onChange={(e) => { onChange={(e) => {
const val = handleDecimalChange(e.target.value, 10) const val = handleIntegerChange(e.target.value)
if (val !== null) { if (val !== null) {
setEditingData({ setEditingData({
...editingData, ...editingData,