a11y: Enhance accessibility across various components by adding ARIA attributes, semantic elements, and input labels.
This commit is contained in:
@@ -33,7 +33,9 @@ export const AnnouncementCard: React.FC<AnnouncementCardProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative p-6 rounded-2xl border transition-all duration-300 ${announcement.is_pinned
|
||||
<article
|
||||
aria-labelledby={`ann-title-${announcement.id}`}
|
||||
className={`relative p-6 rounded-2xl border transition-all duration-300 ${announcement.is_pinned
|
||||
? 'bg-primary-500/10 border-primary-500/30'
|
||||
: 'bg-white/5 border-white/10 hover:border-white/20'
|
||||
}`}>
|
||||
@@ -67,17 +69,18 @@ export const AnnouncementCard: React.FC<AnnouncementCardProps> = ({
|
||||
disabled={isDeleting}
|
||||
className="p-2 rounded-lg hover:bg-red-500/20 text-gray-400 hover:text-red-400 transition-colors"
|
||||
title="Eliminar anuncio"
|
||||
aria-label="Eliminar anuncio"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<Trash2 className="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-white mb-2">{announcement.title}</h3>
|
||||
<h3 id={`ann-title-${announcement.id}`} className="text-xl font-bold text-white mb-2">{announcement.title}</h3>
|
||||
<div className="text-gray-300 whitespace-pre-wrap leading-relaxed">
|
||||
{announcement.content}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,14 +25,14 @@ export default function AppHeader() {
|
||||
<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">
|
||||
{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="" 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" aria-hidden="true">
|
||||
{platformName.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col -gap-1">
|
||||
<div className="flex flex-col -gap-1" aria-hidden="true">
|
||||
<span className="font-black text-sm md:text-lg tracking-tighter text-white leading-none">
|
||||
{platformName.toUpperCase()}
|
||||
</span>
|
||||
@@ -41,7 +41,7 @@ export default function AppHeader() {
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<nav className="hidden md:flex items-center gap-8 mr-4">
|
||||
<nav className="hidden md:flex items-center gap-8 mr-4" aria-label="Navegación principal">
|
||||
<Link href="/" className="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 hover:text-white transition-colors">
|
||||
{t('nav.catalog')}
|
||||
</Link>
|
||||
@@ -106,8 +106,17 @@ export default function AppHeader() {
|
||||
|
||||
{/* Mobile Sidebar Overlay */}
|
||||
{isMenuOpen && (
|
||||
<div className="fixed inset-0 z-[150] md:hidden bg-black/60 backdrop-blur-sm animate-in fade-in 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="fixed inset-0 z-[150] md:hidden bg-black/60 backdrop-blur-sm animate-in fade-in duration-300"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<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"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Menú móvil"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<span className="font-black text-xs uppercase tracking-[0.2em] text-gray-500">Menú</span>
|
||||
<button
|
||||
@@ -119,7 +128,7 @@ export default function AppHeader() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-col gap-6 flex-1">
|
||||
<nav className="flex flex-col gap-6 flex-1" aria-label="Mobile navigation">
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
@@ -175,7 +184,7 @@ export default function AppHeader() {
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center font-bold text-xs text-blue-400">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center font-bold text-xs text-blue-400" aria-hidden="true">
|
||||
{user?.full_name?.charAt(0) || 'U'}
|
||||
</div>
|
||||
<span className="text-sm font-bold">{user?.full_name || 'Mi Perfil'}</span>
|
||||
@@ -184,7 +193,7 @@ export default function AppHeader() {
|
||||
onClick={logout}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
<LogOut size={18} aria-hidden="true" />
|
||||
<span className="text-sm font-bold">{t('nav.signOut')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -56,12 +56,14 @@ export default function InteractiveTranscript({ transcription, currentTime, onSe
|
||||
<div className="flex bg-black/40 rounded-lg p-1 border border-white/5">
|
||||
<button
|
||||
onClick={() => setLang('en')}
|
||||
aria-pressed={lang === 'en'}
|
||||
className={`px-3 py-1 text-[10px] font-black rounded-md transition-all ${lang === 'en' ? 'bg-blue-600 text-white' : 'text-gray-500 hover:text-white'}`}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLang('es')}
|
||||
aria-pressed={lang === 'es'}
|
||||
className={`px-3 py-1 text-[10px] font-black rounded-md transition-all ${lang === 'es' ? 'bg-blue-600 text-white' : 'text-gray-500 hover:text-white'}`}
|
||||
>
|
||||
ES
|
||||
@@ -71,6 +73,9 @@ export default function InteractiveTranscript({ transcription, currentTime, onSe
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
role="region"
|
||||
aria-label="Contenido de la transcripción"
|
||||
aria-live="polite"
|
||||
className="flex-1 overflow-y-auto p-6 space-y-4 custom-scrollbar"
|
||||
>
|
||||
{cues.length === 0 ? (
|
||||
@@ -88,14 +93,23 @@ export default function InteractiveTranscript({ transcription, currentTime, onSe
|
||||
<div
|
||||
key={index}
|
||||
ref={active ? activeCueRef : null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${formatTime(cue.start)}: ${cue.text}`}
|
||||
aria-current={active ? "true" : undefined}
|
||||
onClick={() => onSeek(cue.start)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onSeek(cue.start);
|
||||
}
|
||||
}}
|
||||
className={`group cursor-pointer p-4 rounded-2xl transition-all border ${active
|
||||
? 'bg-blue-500/10 border-blue-500/30 text-white translate-x-1'
|
||||
: 'bg-white/5 border-transparent text-gray-400 hover:bg-white/10 hover:border-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<span className={`text-[10px] font-mono mt-1 ${active ? 'text-blue-400' : 'text-gray-600'}`}>
|
||||
<span className={`text-[10px] font-mono mt-1 ${active ? 'text-blue-400' : 'text-gray-600'}`} aria-hidden="true">
|
||||
{formatTime(cue.start)}
|
||||
</span>
|
||||
<p className={`text-sm leading-relaxed ${active ? 'font-medium' : ''}`}>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { lmsApi, User } from "@/lib/api";
|
||||
import { Trophy, Medal, Award } from "lucide-react";
|
||||
import { Trophy, Medal } from "lucide-react";
|
||||
|
||||
export default function Leaderboard() {
|
||||
const [topUsers, setTopUsers] = useState<User[]>([]);
|
||||
@@ -33,21 +33,21 @@ export default function Leaderboard() {
|
||||
return (
|
||||
<div className="glass-card p-8 border-white/5 bg-white/[0.01] rounded-3xl overflow-hidden relative">
|
||||
<h3 className="text-xs font-black uppercase tracking-widest text-gray-500 mb-6 flex items-center gap-2">
|
||||
<Trophy size={14} className="text-amber-500" /> Tabla de Clasificación
|
||||
<Trophy size={14} className="text-amber-500" aria-hidden="true" /> Tabla de Clasificación
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<ol className="space-y-3" aria-label="Ranking de estudiantes">
|
||||
{topUsers.map((user, index) => (
|
||||
<div
|
||||
<li
|
||||
key={user.id}
|
||||
className={`flex items-center gap-4 p-4 rounded-2xl transition-all ${index === 0 ? 'bg-gradient-to-r from-amber-500/10 to-transparent border border-amber-500/20 shadow-lg shadow-amber-500/5' :
|
||||
'bg-white/5 border border-white/5 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-shrink-0 w-8 text-center font-black text-xs text-gray-600">
|
||||
{index === 0 ? <Medal className="text-amber-500 mx-auto" size={18} /> :
|
||||
index === 1 ? <Medal className="text-gray-400 mx-auto" size={18} /> :
|
||||
index === 2 ? <Medal className="text-amber-700 mx-auto" size={18} /> :
|
||||
<div className="flex-shrink-0 w-8 text-center font-black text-xs text-gray-600" aria-label={`Posición ${index + 1}`}>
|
||||
{index === 0 ? <Medal className="text-amber-500 mx-auto" size={18} aria-hidden="true" /> :
|
||||
index === 1 ? <Medal className="text-gray-400 mx-auto" size={18} aria-hidden="true" /> :
|
||||
index === 2 ? <Medal className="text-amber-700 mx-auto" size={18} aria-hidden="true" /> :
|
||||
index + 1}
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function Leaderboard() {
|
||||
<div className="text-sm font-black text-white">{user.xp || 0}</div>
|
||||
<div className="text-[8px] font-black text-blue-500 uppercase tracking-widest">XP</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
{topUsers.length === 0 && (
|
||||
@@ -68,10 +68,10 @@ export default function Leaderboard() {
|
||||
Aún no hay datos de clasificación disponibles.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ol>
|
||||
|
||||
{/* Visual background flair */}
|
||||
<div className="absolute -top-24 -right-24 w-48 h-48 bg-amber-500/10 blur-[100px] rounded-full pointer-events-none"></div>
|
||||
<div className="absolute -top-24 -right-24 w-48 h-48 bg-amber-500/10 blur-[100px] rounded-full pointer-events-none" aria-hidden="true"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,11 +41,18 @@ export const NewAnnouncementModal: React.FC<NewAnnouncementModalProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-[#1a1c1e] border border-white/10 rounded-3xl w-full max-w-2xl overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-200">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
onClick={onClose}>
|
||||
<div
|
||||
className="bg-[#1a1c1e] border border-white/10 rounded-3xl w-full max-w-2xl overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-200"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-announcement-title"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between p-6 border-b border-white/5">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Send className="w-5 h-5 text-primary-500" />
|
||||
<h2 id="modal-announcement-title" className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Send className="w-5 h-5 text-primary-500" aria-hidden="true" />
|
||||
Publicar Nuevo Anuncio
|
||||
</h2>
|
||||
<button onClick={onClose} className="p-2 hover:bg-white/5 rounded-xl text-gray-400 transition-colors">
|
||||
@@ -62,20 +69,23 @@ export const NewAnnouncementModal: React.FC<NewAnnouncementModalProps> = ({
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-400 ml-1">Título del Anuncio</label>
|
||||
<label htmlFor="ann-title" className="text-sm font-medium text-gray-400 ml-1">Título del Anuncio</label>
|
||||
<input
|
||||
id="ann-title"
|
||||
type="text"
|
||||
required
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Ej: Nuevos materiales disponibles"
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-gray-500 focus:outline-none focus:border-primary-500/50 transition-colors"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-400 ml-1">Contenido</label>
|
||||
<label htmlFor="ann-content" className="text-sm font-medium text-gray-400 ml-1">Contenido</label>
|
||||
<textarea
|
||||
id="ann-content"
|
||||
required
|
||||
rows={6}
|
||||
value={content}
|
||||
|
||||
@@ -46,10 +46,15 @@ export default function NewThreadModal({ courseId, lessonId, onClose, onSuccess
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-slate-900 border border-white/10 rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl">
|
||||
<div
|
||||
className="bg-slate-900 border border-white/10 rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-thread-title"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-white/10">
|
||||
<h2 className="text-2xl font-black text-white">Nuevo Hilo de Discusión</h2>
|
||||
<h2 id="modal-thread-title" className="text-2xl font-black text-white">Nuevo Hilo de Discusión</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center text-gray-400 hover:text-white transition-all"
|
||||
@@ -68,10 +73,11 @@ export default function NewThreadModal({ courseId, lessonId, onClose, onSuccess
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-gray-400 uppercase tracking-wider">
|
||||
<label htmlFor="thread-title" className="text-xs font-bold text-gray-400 uppercase tracking-wider">
|
||||
Título
|
||||
</label>
|
||||
<input
|
||||
id="thread-title"
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
@@ -79,19 +85,21 @@ export default function NewThreadModal({ courseId, lessonId, onClose, onSuccess
|
||||
className="w-full bg-slate-900/50 border border-white/10 rounded-xl py-3 px-4 text-white text-sm focus:border-indigo-500 focus:outline-none transition-colors"
|
||||
disabled={submitting}
|
||||
maxLength={200}
|
||||
autoFocus
|
||||
/>
|
||||
<p className="text-xs text-gray-500">{title.length}/200 caracteres</p>
|
||||
<p id="thread-title-hint" className="text-xs text-gray-500">{title.length}/200 caracteres</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-gray-400 uppercase tracking-wider">
|
||||
<label htmlFor="thread-content" className="text-xs font-bold text-gray-400 uppercase tracking-wider">
|
||||
Contenido
|
||||
</label>
|
||||
<textarea
|
||||
id="thread-content"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Describe tu pregunta o tema en detalle..."
|
||||
placeholder="Describe tu pregunta o tema en detail..."
|
||||
className="w-full bg-slate-900/50 border border-white/10 rounded-xl py-3 px-4 text-white text-sm focus:border-indigo-500 focus:outline-none transition-colors resize-none"
|
||||
rows={8}
|
||||
disabled={submitting}
|
||||
|
||||
@@ -149,6 +149,7 @@ export default function NotificationCenter() {
|
||||
<div className="p-4 border-t border-white/5 bg-white/5 text-center">
|
||||
<button
|
||||
className="text-[10px] font-black uppercase tracking-widest text-gray-500 hover:text-white transition-colors"
|
||||
aria-label="Marcar todas las notificaciones como leídas"
|
||||
onClick={() => {
|
||||
notifications.filter(n => !n.is_read).forEach(n => markAsRead(n.id));
|
||||
}}
|
||||
|
||||
@@ -32,14 +32,17 @@ export default function PostCard({ post, onReply, onVote, onEndorse, depth, isIn
|
||||
const indentClass = depth > 0 ? `ml-${Math.min(depth * 4, 16)} pl-4 border-l-2 border-white/10` : '';
|
||||
|
||||
return (
|
||||
<div className={`${indentClass} ${depth > 0 ? 'mt-4' : 'mt-6'}`}>
|
||||
<article
|
||||
className={`${indentClass} ${depth > 0 ? 'mt-4' : 'mt-6'}`}
|
||||
aria-labelledby={`post-content-${post.id}`}
|
||||
>
|
||||
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-4 hover:bg-white/[0.07] transition-all">
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
{/* Avatar */}
|
||||
<div className="w-10 h-10 rounded-full bg-indigo-600/20 flex items-center justify-center text-indigo-400 flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-indigo-600/20 flex items-center justify-center text-indigo-400 flex-shrink-0" aria-hidden="true">
|
||||
{post.author_avatar ? (
|
||||
<img src={post.author_avatar} alt={post.author_name} className="w-full h-full rounded-full object-cover" />
|
||||
<img src={post.author_avatar} alt="" className="w-full h-full rounded-full object-cover" />
|
||||
) : (
|
||||
<User size={20} />
|
||||
)}
|
||||
@@ -63,7 +66,7 @@ export default function PostCard({ post, onReply, onVote, onEndorse, depth, isIn
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="text-gray-300 text-sm mb-4 whitespace-pre-wrap break-words">
|
||||
<div id={`post-content-${post.id}`} className="text-gray-300 text-sm mb-4 whitespace-pre-wrap break-words">
|
||||
{post.content}
|
||||
</div>
|
||||
|
||||
@@ -73,12 +76,14 @@ export default function PostCard({ post, onReply, onVote, onEndorse, depth, isIn
|
||||
<button
|
||||
onClick={() => handleVote('upvote')}
|
||||
disabled={isVoting}
|
||||
aria-label={`Votar positivo (${post.upvotes} votos)`}
|
||||
aria-pressed={post.user_vote === 'upvote'}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all ${post.user_vote === 'upvote'
|
||||
? 'bg-indigo-600/30 text-indigo-400 border border-indigo-500/50'
|
||||
: 'bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white border border-white/10'
|
||||
} disabled:opacity-50`}
|
||||
>
|
||||
<ThumbsUp size={14} />
|
||||
<ThumbsUp size={14} aria-hidden="true" />
|
||||
<span className="font-bold">{post.upvotes}</span>
|
||||
</button>
|
||||
|
||||
@@ -86,9 +91,10 @@ export default function PostCard({ post, onReply, onVote, onEndorse, depth, isIn
|
||||
{depth < maxDepth && (
|
||||
<button
|
||||
onClick={() => onReply(post.id)}
|
||||
aria-label={`Responder al post de ${post.author_name}`}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white border border-white/10 transition-all"
|
||||
>
|
||||
<MessageSquare size={14} />
|
||||
<MessageSquare size={14} aria-hidden="true" />
|
||||
<span className="font-bold">Responder</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -97,12 +103,14 @@ export default function PostCard({ post, onReply, onVote, onEndorse, depth, isIn
|
||||
{isInstructor && onEndorse && (
|
||||
<button
|
||||
onClick={() => onEndorse(post.id)}
|
||||
aria-label={post.is_endorsed ? 'Quitar aprobación de respuesta' : 'Marcar como respuesta correcta'}
|
||||
aria-pressed={!!post.is_endorsed}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all ${post.is_endorsed
|
||||
? 'bg-green-600/30 text-green-400 border border-green-500/50'
|
||||
: 'bg-white/5 text-gray-400 hover:bg-green-600/20 hover:text-green-400 border border-white/10'
|
||||
}`}
|
||||
>
|
||||
<CheckCircle2 size={14} />
|
||||
<CheckCircle2 size={14} aria-hidden="true" />
|
||||
<span className="font-bold text-xs">{post.is_endorsed ? 'Aprobada' : 'Aprobar'}</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -125,6 +133,6 @@ export default function PostCard({ post, onReply, onVote, onEndorse, depth, isIn
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,17 +35,20 @@ export default function Modal({ isOpen, onClose, title, children }: ModalProps)
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
className="w-full max-w-md glass-card bg-[#1a1d23] border border-white/10 rounded-2xl p-8 shadow-2xl animate-in zoom-in-95 duration-200"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-bold bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent italic">
|
||||
<h2 id="modal-title" className="text-xl font-bold bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent italic">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-white/5 rounded-full transition-colors text-gray-400 hover:text-white"
|
||||
>
|
||||
<X size={20} />
|
||||
<X size={20} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user