feat: Adjust roadmap priorities to elevate accessibility and implement ARIA attributes in AppHeader for improved screen reader support.
This commit is contained in:
+1
-2
@@ -115,7 +115,6 @@
|
|||||||
- [x] **Ecosistema de Integración**:
|
- [x] **Ecosistema de Integración**:
|
||||||
- [x] **SSO (Single Sign-On)**: Soporte completo OIDC (Google, Okta, Azure AD) (Completado)
|
- [x] **SSO (Single Sign-On)**: Soporte completo OIDC (Google, Okta, Azure AD) (Completado)
|
||||||
- [x] **LTI 1.3 Tool Provider**: Integración segura con LMS externos como Canvas o Moodle (Completado)
|
- [x] **LTI 1.3 Tool Provider**: Integración segura con LMS externos como Canvas o Moodle (Completado)
|
||||||
- [ ] **Apps Móviles**: (Postpuesto por ahora)
|
|
||||||
- [ ] **Accesibilidad**: Auditoría y correcciones WCAG 2.1
|
- [ ] **Accesibilidad**: Auditoría y correcciones WCAG 2.1
|
||||||
|
|
||||||
## Fase 9: Portabilidad de Cursos ✅
|
## Fase 9: Portabilidad de Cursos ✅
|
||||||
@@ -224,6 +223,6 @@
|
|||||||
**Estado Actual**: La plataforma cuenta con un motor de IA avanzado, gestión multi-tenant completa, tutoría inteligente con memoria histórica, una **interfaz 100% responsiva**, flujos de autenticación diferenciados, **sistema de foros de discusión funcional**, **gestión de anuncios segmentados**, **monetización integrada con Mercado Pago**, **Inscripción Masiva de Usuarios**, **Exportación Avanzada de Calificaciones**, **Librerías de Contenido reutilizables**, **Sistema de Rúbricas Avanzado**, **Secuencias de Aprendizaje**, **Gestión de Equipos Docentes**, **Vista Previa de Cursos**, **Dashboard de Progreso Estudiantil**, **Sistema de Marcadores**, **Biblioteca Global de Activos**, **Interoperabilidad LTI 1.3 con soporte para Deep Linking**, **Analíticas Predictivas de Riesgo de Abandono**, **Integración de Videoconferencia (Jitsi)** y **Portafolios con Perfiles Públicos**.
|
**Estado Actual**: La plataforma cuenta con un motor de IA avanzado, gestión multi-tenant completa, tutoría inteligente con memoria histórica, una **interfaz 100% responsiva**, flujos de autenticación diferenciados, **sistema de foros de discusión funcional**, **gestión de anuncios segmentados**, **monetización integrada con Mercado Pago**, **Inscripción Masiva de Usuarios**, **Exportación Avanzada de Calificaciones**, **Librerías de Contenido reutilizables**, **Sistema de Rúbricas Avanzado**, **Secuencias de Aprendizaje**, **Gestión de Equipos Docentes**, **Vista Previa de Cursos**, **Dashboard de Progreso Estudiantil**, **Sistema de Marcadores**, **Biblioteca Global de Activos**, **Interoperabilidad LTI 1.3 con soporte para Deep Linking**, **Analíticas Predictivas de Riesgo de Abandono**, **Integración de Videoconferencia (Jitsi)** y **Portafolios con Perfiles Públicos**.
|
||||||
|
|
||||||
**Próximas Prioridades**:
|
**Próximas Prioridades**:
|
||||||
1. **Apps Móviles**: Desarrollo de versiones nativas para iOS y Android.
|
1. **Accesibilidad Universal**: Auditoría y ajustes de contraste para cumplimiento WCAG 2.1.
|
||||||
2. **Integraciones Empresariales**: Conectividad con HRIS y ERPs externos.
|
2. **Integraciones Empresariales**: Conectividad con HRIS y ERPs externos.
|
||||||
3. **IA Generativa Avanzada**: Generación automática de contenido de video a partir de guiones.
|
3. **IA Generativa Avanzada**: Generación automática de contenido de video a partir de guiones.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function AITutor({ lessonId }: { lessonId: string }) {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLUListElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load session from localStorage on mount
|
// Load session from localStorage on mount
|
||||||
@@ -62,19 +62,24 @@ export default function AITutor({ lessonId }: { lessonId: string }) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
className="fixed bottom-24 right-6 w-14 h-14 rounded-2xl bg-blue-600 text-white shadow-lg shadow-blue-500/40 flex items-center justify-center hover:scale-110 transition-all z-[100] group"
|
className="fixed bottom-24 right-6 w-14 h-14 rounded-2xl bg-blue-600 text-white shadow-lg shadow-blue-500/40 flex items-center justify-center hover:scale-110 transition-all z-[100] group"
|
||||||
title="Abrir Tutor de IA"
|
aria-label="Abrir Tutor de IA"
|
||||||
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 border-2 border-black rounded-full animate-pulse" />
|
<div className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 border-2 border-black rounded-full animate-pulse" aria-hidden="true" />
|
||||||
<MessageSquare className="w-6 h-6 group-hover:rotate-12 transition-transform" />
|
<MessageSquare className="w-6 h-6 group-hover:rotate-12 transition-transform" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-24 right-6 w-80 md:w-96 h-[500px] glass bg-black/80 backdrop-blur-2xl border border-white/10 rounded-3xl shadow-2xl flex flex-col z-[200] animate-in slide-in-from-bottom-6 duration-500 overflow-hidden">
|
<div
|
||||||
|
className="fixed bottom-24 right-6 w-80 md:w-96 h-[500px] glass bg-black/80 backdrop-blur-2xl border border-white/10 rounded-3xl shadow-2xl flex flex-col z-[200] animate-in slide-in-from-bottom-6 duration-500 overflow-hidden"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Tutor de IA"
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="p-4 border-b border-white/5 bg-blue-600/10 flex items-center justify-between">
|
<div className="p-4 border-b border-white/5 bg-blue-600/10 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3" aria-hidden="true">
|
||||||
<div className="w-10 h-10 rounded-xl bg-blue-500 flex items-center justify-center shadow-lg shadow-blue-500/20">
|
<div className="w-10 h-10 rounded-xl bg-blue-500 flex items-center justify-center shadow-lg shadow-blue-500/20">
|
||||||
<Bot className="w-6 h-6 text-white" />
|
<Bot className="w-6 h-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
@@ -89,38 +94,43 @@ export default function AITutor({ lessonId }: { lessonId: string }) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className="p-2 hover:bg-white/5 rounded-lg text-gray-400 hover:text-white transition-colors"
|
className="p-2 hover:bg-white/5 rounded-lg text-gray-400 hover:text-white transition-colors"
|
||||||
|
aria-label="Cerrar Tutor de IA"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div
|
<ul
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hide"
|
className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hide"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-relevant="additions"
|
||||||
>
|
>
|
||||||
{messages.map((msg, i) => (
|
{messages.map((msg, i) => (
|
||||||
<div
|
<li
|
||||||
key={i}
|
key={i}
|
||||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
>
|
>
|
||||||
<div className={`flex gap-2 max-w-[85%] ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}>
|
<div className={`flex gap-2 max-w-[85%] ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}>
|
||||||
<div className={`shrink-0 w-8 h-8 rounded-lg flex items-center justify-center ${msg.role === 'user' ? 'bg-white/5' : 'bg-blue-600/20 text-blue-400'}`}>
|
<div className={`shrink-0 w-8 h-8 rounded-lg flex items-center justify-center ${msg.role === 'user' ? 'bg-white/5' : 'bg-blue-600/20 text-blue-400'}`} aria-hidden="true">
|
||||||
{msg.role === 'user' ? <User size={16} /> : <Bot size={16} />}
|
{msg.role === 'user' ? <User size={16} /> : <Bot size={16} />}
|
||||||
</div>
|
</div>
|
||||||
<div className={`p-3 rounded-2xl text-xs font-medium leading-relaxed ${msg.role === 'user'
|
<div className={`p-3 rounded-2xl text-xs font-medium leading-relaxed ${msg.role === 'user'
|
||||||
? 'bg-blue-600 text-white rounded-tr-none'
|
? 'bg-blue-600 text-white rounded-tr-none'
|
||||||
: 'bg-white/5 text-gray-200 border border-white/5 rounded-tl-none'
|
: 'bg-white/5 text-gray-200 border border-white/5 rounded-tl-none'
|
||||||
}`}>
|
}`}
|
||||||
|
aria-label={msg.role === 'user' ? 'Tú dijiste' : 'El tutor dijo'}
|
||||||
|
>
|
||||||
{msg.content}
|
{msg.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
))}
|
))}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex justify-start animate-in fade-in duration-300">
|
<li className="flex justify-start animate-in fade-in duration-300" aria-busy="true">
|
||||||
<div className="flex gap-2 max-w-[85%]">
|
<div className="flex gap-2 max-w-[85%]">
|
||||||
<div className="shrink-0 w-8 h-8 rounded-lg bg-blue-600/20 text-blue-400 flex items-center justify-center">
|
<div className="shrink-0 w-8 h-8 rounded-lg bg-blue-600/20 text-blue-400 flex items-center justify-center" aria-hidden="true">
|
||||||
<Bot size={16} />
|
<Bot size={16} />
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/5 text-gray-400 border border-white/5 p-3 rounded-2xl rounded-tl-none flex items-center gap-2">
|
<div className="bg-white/5 text-gray-400 border border-white/5 p-3 rounded-2xl rounded-tl-none flex items-center gap-2">
|
||||||
@@ -128,9 +138,9 @@ export default function AITutor({ lessonId }: { lessonId: string }) {
|
|||||||
<span className="text-[10px] font-bold uppercase tracking-widest">El tutor está pensando...</span>
|
<span className="text-[10px] font-bold uppercase tracking-widest">El tutor está pensando...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ul>
|
||||||
|
|
||||||
{/* Input */}
|
{/* Input */}
|
||||||
<div className="p-4 border-t border-white/5 bg-black/40">
|
<div className="p-4 border-t border-white/5 bg-black/40">
|
||||||
@@ -141,14 +151,16 @@ export default function AITutor({ lessonId }: { lessonId: string }) {
|
|||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||||
placeholder="Escribe tu duda aquí..."
|
placeholder="Escribe tu duda aquí..."
|
||||||
|
aria-label="Escribe tu duda para el tutor"
|
||||||
className="w-full bg-white/5 border border-white/10 rounded-xl py-3 px-4 pr-12 text-xs font-medium focus:outline-none focus:border-blue-500/50 transition-colors placeholder:text-gray-600"
|
className="w-full bg-white/5 border border-white/10 rounded-xl py-3 px-4 pr-12 text-xs font-medium focus:outline-none focus:border-blue-500/50 transition-colors placeholder:text-gray-600"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={isLoading || !input.trim()}
|
disabled={isLoading || !input.trim()}
|
||||||
className="absolute right-2 top-1.5 p-1.5 bg-blue-600 text-white rounded-lg disabled:opacity-50 disabled:bg-gray-600 transition-all hover:bg-blue-500"
|
className="absolute right-2 top-1.5 p-1.5 bg-blue-600 text-white rounded-lg disabled:opacity-50 disabled:bg-gray-600 transition-all hover:bg-blue-500"
|
||||||
|
aria-label="Enviar mensaje"
|
||||||
>
|
>
|
||||||
<Send size={16} />
|
<Send size={16} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-[9px] text-gray-600 font-bold uppercase tracking-widest text-center">
|
<p className="mt-2 text-[9px] text-gray-600 font-bold uppercase tracking-widest text-center">
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ export default function AppHeader() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="h-16 glass sticky top-0 z-[100] px-4 md:px-6 flex items-center justify-between backdrop-blur-xl bg-black/40 border-b border-white/5">
|
<header className="h-16 glass sticky top-0 z-[100] px-4 md:px-6 flex items-center justify-between backdrop-blur-xl bg-black/40 border-b border-white/5">
|
||||||
<Link href="/" className="flex items-center gap-2 md:gap-3 group">
|
<Link href="/" className="flex items-center gap-2 md:gap-3 group" aria-label={`${platformName} - Dashboard`}>
|
||||||
<div className="w-8 h-8 md:w-10 md:h-10 rounded-lg md:rounded-xl bg-blue-600 flex items-center justify-center font-black text-white shadow-lg shadow-blue-500/20 group-hover:scale-105 transition-all overflow-hidden relative">
|
<div className="w-8 h-8 md:w-10 md:h-10 rounded-lg md:rounded-xl bg-blue-600 flex items-center justify-center font-black text-white shadow-lg shadow-blue-500/20 group-hover:scale-105 transition-all overflow-hidden relative">
|
||||||
{branding?.logo_url ? (
|
{branding?.logo_url ? (
|
||||||
<Image src={getImageUrl(branding.logo_url)} alt={branding.name} fill className="object-contain" sizes="40px" />
|
<Image src={getImageUrl(branding.logo_url)} alt={branding.name} fill className="object-contain" sizes="40px" />
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-blue-500 to-blue-700">
|
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-blue-500 to-blue-700" aria-hidden="true">
|
||||||
{platformName.charAt(0).toUpperCase()}
|
{platformName.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -62,10 +62,12 @@ export default function AppHeader() {
|
|||||||
<NotificationCenter />
|
<NotificationCenter />
|
||||||
|
|
||||||
<div className="hidden sm:flex items-center gap-2 border-l border-white/10 pl-4">
|
<div className="hidden sm:flex items-center gap-2 border-l border-white/10 pl-4">
|
||||||
<Globe size={14} className="text-gray-500" />
|
<Globe size={14} className="text-gray-500" aria-hidden="true" />
|
||||||
<select
|
<select
|
||||||
|
id="language-selector"
|
||||||
value={language}
|
value={language}
|
||||||
onChange={(e) => setLanguage(e.target.value)}
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
|
aria-label={t('nav.selectLanguage') || 'Select Language'}
|
||||||
className="bg-transparent text-[10px] font-black uppercase tracking-widest text-gray-500 hover:text-white transition-colors focus:outline-none cursor-pointer"
|
className="bg-transparent text-[10px] font-black uppercase tracking-widest text-gray-500 hover:text-white transition-colors focus:outline-none cursor-pointer"
|
||||||
>
|
>
|
||||||
<option value="en" className="bg-[#0f1115]">EN</option>
|
<option value="en" className="bg-[#0f1115]">EN</option>
|
||||||
@@ -84,6 +86,7 @@ export default function AppHeader() {
|
|||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="p-2 hover:bg-red-500/10 rounded-full text-gray-400 hover:text-red-400 transition-colors"
|
className="p-2 hover:bg-red-500/10 rounded-full text-gray-400 hover:text-red-400 transition-colors"
|
||||||
title={t('nav.signOut')}
|
title={t('nav.signOut')}
|
||||||
|
aria-label={t('nav.signOut')}
|
||||||
>
|
>
|
||||||
<LogOut size={16} />
|
<LogOut size={16} />
|
||||||
</button>
|
</button>
|
||||||
@@ -93,6 +96,8 @@ export default function AppHeader() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
className="md:hidden p-2 hover:bg-white/5 rounded-lg text-gray-400 transition-colors"
|
className="md:hidden p-2 hover:bg-white/5 rounded-lg text-gray-400 transition-colors"
|
||||||
|
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||||
|
aria-expanded={isMenuOpen}
|
||||||
>
|
>
|
||||||
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||||
</button>
|
</button>
|
||||||
@@ -105,7 +110,11 @@ export default function AppHeader() {
|
|||||||
<div className="absolute right-0 top-0 bottom-0 w-64 glass border-l border-white/10 p-6 flex flex-col animate-in slide-in-from-right duration-300">
|
<div className="absolute right-0 top-0 bottom-0 w-64 glass border-l border-white/10 p-6 flex flex-col animate-in slide-in-from-right duration-300">
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className="flex justify-between items-center mb-8">
|
||||||
<span className="font-black text-xs uppercase tracking-[0.2em] text-gray-500">Menú</span>
|
<span className="font-black text-xs uppercase tracking-[0.2em] text-gray-500">Menú</span>
|
||||||
<button onClick={() => setIsMenuOpen(false)} className="p-2 hover:bg-white/5 rounded-lg">
|
<button
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
className="p-2 hover:bg-white/5 rounded-lg"
|
||||||
|
aria-label="Close menu"
|
||||||
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,10 +153,12 @@ export default function AppHeader() {
|
|||||||
|
|
||||||
<div className="pt-6 mt-6 border-t border-white/5 space-y-4">
|
<div className="pt-6 mt-6 border-t border-white/5 space-y-4">
|
||||||
<div className="flex items-center gap-3 px-4 py-2 rounded-xl bg-white/5">
|
<div className="flex items-center gap-3 px-4 py-2 rounded-xl bg-white/5">
|
||||||
<Globe size={16} className="text-gray-500" />
|
<Globe size={16} className="text-gray-500" aria-hidden="true" />
|
||||||
<select
|
<select
|
||||||
|
id="mobile-language-selector"
|
||||||
value={language}
|
value={language}
|
||||||
onChange={(e) => setLanguage(e.target.value)}
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
|
aria-label={t('nav.selectLanguage') || 'Select Language'}
|
||||||
className="bg-transparent text-xs font-bold uppercase tracking-widest text-gray-300 focus:outline-none flex-1"
|
className="bg-transparent text-xs font-bold uppercase tracking-widest text-gray-300 focus:outline-none flex-1"
|
||||||
>
|
>
|
||||||
<option value="en" className="bg-[#0f1115]">English</option>
|
<option value="en" className="bg-[#0f1115]">English</option>
|
||||||
|
|||||||
@@ -60,10 +60,16 @@ export default function NotificationCenter() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className="relative p-2 rounded-xl glass border-white/10 text-gray-400 hover:text-white transition-all hover:bg-white/5"
|
className="relative p-2 rounded-xl glass border-white/10 text-gray-400 hover:text-white transition-all hover:bg-white/5"
|
||||||
|
aria-label={`Notifications ${unreadCount > 0 ? `(${unreadCount} unread)` : ''}`}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={isOpen}
|
||||||
>
|
>
|
||||||
<Bell size={20} />
|
<Bell size={20} aria-hidden="true" />
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<span className="absolute top-0 right-0 w-5 h-5 bg-red-500 text-white text-[10px] font-black rounded-full flex items-center justify-center border-2 border-[#1a1c20] -translate-to-1/4">
|
<span
|
||||||
|
className="absolute top-0 right-0 w-5 h-5 bg-red-500 text-white text-[10px] font-black rounded-full flex items-center justify-center border-2 border-[#1a1c20] -translate-to-1/4"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
{unreadCount}
|
{unreadCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -72,10 +78,18 @@ export default function NotificationCenter() {
|
|||||||
{isOpen && (
|
{isOpen && (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
|
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
|
||||||
<div className="absolute right-0 mt-4 w-96 glass-card border-white/10 z-50 shadow-2xl animate-in fade-in zoom-in-95 duration-200 overflow-hidden bg-[#1a1c20]/95 backdrop-blur-xl">
|
<div
|
||||||
|
className="absolute right-0 mt-4 w-96 glass-card border-white/10 z-50 shadow-2xl animate-in fade-in zoom-in-95 duration-200 overflow-hidden bg-[#1a1c20]/95 backdrop-blur-xl"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Notifications Panel"
|
||||||
|
>
|
||||||
<div className="p-4 border-b border-white/5 flex items-center justify-between bg-white/5">
|
<div className="p-4 border-b border-white/5 flex items-center justify-between bg-white/5">
|
||||||
<h3 className="text-sm font-black uppercase tracking-widest text-white">Notificaciones</h3>
|
<h3 className="text-sm font-black uppercase tracking-widest text-white">Notificaciones</h3>
|
||||||
<button onClick={() => setIsOpen(false)} className="text-gray-500 hover:text-white p-1">
|
<button
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="text-gray-500 hover:text-white p-1"
|
||||||
|
aria-label="Close notifications"
|
||||||
|
>
|
||||||
<X size={18} />
|
<X size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,15 +100,23 @@ export default function NotificationCenter() {
|
|||||||
) : notifications.length === 0 ? (
|
) : notifications.length === 0 ? (
|
||||||
<div className="p-12 text-center text-gray-500 text-xs font-bold uppercase tracking-widest italic">No hay notificaciones</div>
|
<div className="p-12 text-center text-gray-500 text-xs font-bold uppercase tracking-widest italic">No hay notificaciones</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-white/5">
|
<ul className="divide-y divide-white/5">
|
||||||
{notifications.map((n) => (
|
{notifications.map((n) => (
|
||||||
<div
|
<li
|
||||||
key={n.id}
|
key={n.id}
|
||||||
className={`p-4 hover:bg-white/5 transition-all group ${!n.is_read ? 'bg-blue-500/5' : ''}`}
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={`p-4 hover:bg-white/5 transition-all group cursor-pointer outline-none focus:bg-white/5 ${!n.is_read ? 'bg-blue-500/5' : ''}`}
|
||||||
onClick={() => !n.is_read && markAsRead(n.id)}
|
onClick={() => !n.is_read && markAsRead(n.id)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
!n.is_read && markAsRead(n.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={`Notification: ${n.title}. ${n.message}`}
|
||||||
>
|
>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="mt-1">{getIcon(n.notification_type)}</div>
|
<div className="mt-1" aria-hidden="true">{getIcon(n.notification_type)}</div>
|
||||||
<div className="flex-1 space-y-1">
|
<div className="flex-1 space-y-1">
|
||||||
<p className="text-sm font-bold text-white leading-tight">{n.title}</p>
|
<p className="text-sm font-bold text-white leading-tight">{n.title}</p>
|
||||||
<p className="text-xs text-gray-400 leading-relaxed">{n.message}</p>
|
<p className="text-xs text-gray-400 leading-relaxed">{n.message}</p>
|
||||||
@@ -106,7 +128,10 @@ export default function NotificationCenter() {
|
|||||||
<Link
|
<Link
|
||||||
href={n.link_url}
|
href={n.link_url}
|
||||||
className="text-[10px] font-black uppercase tracking-widest text-blue-400 hover:text-blue-300 flex items-center gap-1"
|
className="text-[10px] font-black uppercase tracking-widest text-blue-400 hover:text-blue-300 flex items-center gap-1"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Ver detalles →
|
Ver detalles →
|
||||||
</Link>
|
</Link>
|
||||||
@@ -114,9 +139,9 @@ export default function NotificationCenter() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -76,9 +76,13 @@ export default function QuizPlayer({ id, title, quizData, allowRetry = true, max
|
|||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{questions.map((q) => (
|
{questions.map((q) => (
|
||||||
<div key={q.id} className="space-y-4 p-8 glass border-white/5 rounded-3xl">
|
<fieldset key={q.id} className="space-y-4 p-8 glass border-white/5 rounded-3xl" aria-labelledby={`q-${q.id}-text`}>
|
||||||
<h4 className="font-bold text-xl text-gray-100 leading-tight">{q.question}</h4>
|
<legend id={`q-${q.id}-text`} className="font-bold text-xl text-gray-100 leading-tight mb-4">{q.question}</legend>
|
||||||
<div className="grid gap-3">
|
<div
|
||||||
|
className="grid gap-3"
|
||||||
|
role={q.type === 'multiple-select' ? 'group' : 'radiogroup'}
|
||||||
|
aria-label="Opciones de respuesta"
|
||||||
|
>
|
||||||
{q.options.map((opt, oIdx) => {
|
{q.options.map((opt, oIdx) => {
|
||||||
const isSelected = userAnswers[q.id]?.includes(oIdx);
|
const isSelected = userAnswers[q.id]?.includes(oIdx);
|
||||||
const isCorrect = q.correct?.includes(oIdx);
|
const isCorrect = q.correct?.includes(oIdx);
|
||||||
@@ -99,20 +103,27 @@ export default function QuizPlayer({ id, title, quizData, allowRetry = true, max
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={oIdx}
|
key={oIdx}
|
||||||
|
role={q.type === 'multiple-select' ? 'checkbox' : 'radio'}
|
||||||
|
aria-checked={isSelected}
|
||||||
|
aria-disabled={submitted}
|
||||||
onClick={() => handleAnswer(q.id, oIdx, q.type === 'multiple-select')}
|
onClick={() => handleAnswer(q.id, oIdx, q.type === 'multiple-select')}
|
||||||
className={`p-5 rounded-xl border transition-all text-left text-sm font-bold ${style}`}
|
className={`p-5 rounded-xl border transition-all text-left text-sm font-bold outline-none focus:ring-2 focus:ring-blue-500/50 ${style}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>{opt}</span>
|
<span>{opt}</span>
|
||||||
{submitted && isActuallyCorrect && <span>✅</span>}
|
{submitted && (
|
||||||
{submitted && isWrongSelection && <span>❌</span>}
|
<div className="flex items-center gap-2">
|
||||||
{submitted && missedCorrect && <span className="text-[10px] uppercase font-black tracking-tighter">Respuesta Correcta</span>}
|
{isActuallyCorrect && <span role="img" aria-label="Correcto">✅</span>}
|
||||||
|
{isWrongSelection && <span role="img" aria-label="Incorrecto">❌</span>}
|
||||||
|
{missedCorrect && <span className="text-[10px] uppercase font-black tracking-tighter text-orange-400">Respuesta Correcta</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</fieldset>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{allowRetry && (
|
{allowRetry && (
|
||||||
|
|||||||
Reference in New Issue
Block a user