initial commit
6
static/css/bootstrap/bootstrap.min.css
vendored
Normal file
1
static/css/bootstrap/bootstrap.min.css.map
Normal file
798
static/css/custom.css
Normal file
@@ -0,0 +1,798 @@
|
||||
/* Estilos personalizados para el Balotario */
|
||||
|
||||
:root {
|
||||
--primary-color: #2c3e50;
|
||||
--secondary-color: #3498db;
|
||||
--success-color: #27ae60;
|
||||
--danger-color: #e74c3c;
|
||||
--warning-color: #f39c1
|
||||
-light-bg: #ecf0f1;
|
||||
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--gradient-success: linear-gradient(45deg, #27ae60, #58d68d);
|
||||
--gradient-danger: linear-gradient(45deg, #e74c3c, #ec7063);
|
||||
--gradient-warning: linear-gradient(45deg, #f39c12, #f7dc6f);
|
||||
}
|
||||
|
||||
/* Animaciones personalizadas */
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Efectos de hover mejorados */
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Botones con efectos especiales */
|
||||
.btn-glow {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-glow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn-glow:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
/* Indicadores de progreso mejorados */
|
||||
.progress-ring {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-ring svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.progress-ring circle {
|
||||
fill: none;
|
||||
stroke-width: 8;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.progress-ring .bg {
|
||||
stroke: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.progress-ring .progress {
|
||||
stroke: #fff;
|
||||
stroke-dasharray: 283;
|
||||
stroke-dashoffset: 283;
|
||||
transition: stroke-dashoffset 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Efectos de texto */
|
||||
.text-glow {
|
||||
text-shadow: 0 0 10px rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Tarjetas de preguntas mejoradas - tema claro por defecto */
|
||||
.question-card-enhanced {
|
||||
background: white;
|
||||
color: #333333;
|
||||
border-radius: 20px;
|
||||
padding: 2.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 15px 35px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.question-card-enhanced::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
/* Opciones de respuesta mejoradas */
|
||||
.option-enhanced {
|
||||
background: white;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.option-enhanced::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(52, 152, 219, 0.1), transparent);
|
||||
transition: left 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.option-enhanced:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.option-enhanced:hover {
|
||||
border-color: var(--secondary-color);
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
|
||||
.option-enhanced.selected {
|
||||
border-color: var(--secondary-color);
|
||||
background: rgba(52, 152, 219, 0.25);
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
|
||||
}
|
||||
|
||||
.option-enhanced.correct {
|
||||
border-color: var(--success-color);
|
||||
background: rgba(39, 174, 96, 0.25);
|
||||
animation: pulse 0.6s ease-in-out;
|
||||
color: #1e5631;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.option-enhanced.incorrect {
|
||||
border-color: var(--danger-color);
|
||||
background: rgba(231, 76, 60, 0.25);
|
||||
color: #721c24;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Navegador de preguntas mejorado */
|
||||
.question-navigator {
|
||||
background: rgba(255,255,255,0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.nav-question-btn {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e9ecef;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.nav-question-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.nav-question-btn.answered {
|
||||
background: var(--gradient-success);
|
||||
border-color: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-question-btn.current {
|
||||
background: var(--gradient-primary);
|
||||
border-color: var(--secondary-color);
|
||||
color: white;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* Timer mejorado */
|
||||
.timer-display {
|
||||
background: var(--gradient-warning);
|
||||
color: white;
|
||||
padding: 15px 25px;
|
||||
border-radius: 25px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 25px rgba(243, 156, 18, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timer-display.warning {
|
||||
background: var(--gradient-danger);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
/* Estadísticas mejoradas */
|
||||
.stats-enhanced {
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-enhanced::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||||
animation: rotate 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* Responsive mejoras */
|
||||
@media (max-width: 768px) {
|
||||
.question-card-enhanced {
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.option-enhanced {
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-question-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.timer-display {
|
||||
font-size: 1.2rem;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tema claro - respuestas seleccionadas más intensas */
|
||||
[data-theme="light"] .option-enhanced.selected,
|
||||
:root:not([data-theme="dark"]) .option-enhanced.selected {
|
||||
border-color: var(--secondary-color);
|
||||
background: rgba(52, 152, 219, 0.35);
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
[data-theme="light"] .option-btn.selected,
|
||||
:root:not([data-theme="dark"]) .option-btn.selected {
|
||||
background: rgba(52, 152, 219, 0.35) !important;
|
||||
border-color: var(--secondary-color) !important;
|
||||
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4) !important;
|
||||
border-width: 3px !important;
|
||||
}
|
||||
|
||||
/* Tema claro - respuestas correctas más intensas */
|
||||
[data-theme="light"] .option-enhanced.correct,
|
||||
:root:not([data-theme="dark"]) .option-enhanced.correct {
|
||||
border-color: #1e7e34;
|
||||
background: rgba(39, 174, 96, 0.4);
|
||||
color: #0d4016;
|
||||
font-weight: 700;
|
||||
border-width: 3px;
|
||||
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="light"] .option-btn.correct,
|
||||
:root:not([data-theme="dark"]) .option-btn.correct {
|
||||
background: rgba(39, 174, 96, 0.4) !important;
|
||||
border-color: #1e7e34 !important;
|
||||
color: #0d4016 !important;
|
||||
font-weight: 700 !important;
|
||||
border-width: 3px !important;
|
||||
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Tema claro - respuestas incorrectas más intensas */
|
||||
[data-theme="light"] .option-enhanced.incorrect,
|
||||
:root:not([data-theme="dark"]) .option-enhanced.incorrect {
|
||||
border-color: #c82333;
|
||||
background: rgba(231, 76, 60, 0.4);
|
||||
color: #721c24;
|
||||
font-weight: 700;
|
||||
border-width: 3px;
|
||||
box-shadow: 0 6px 20px rgba(231, 76, 60, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="light"] .option-btn.incorrect,
|
||||
:root:not([data-theme="dark"]) .option-btn.incorrect {
|
||||
background: rgba(231, 76, 60, 0.4) !important;
|
||||
border-color: #c82333 !important;
|
||||
color: #721c24 !important;
|
||||
font-weight: 700 !important;
|
||||
border-width: 3px !important;
|
||||
box-shadow: 0 6px 20px rgba(231, 76, 60, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Modal support for light theme - ensuring proper styling */
|
||||
[data-theme="light"] .modal-content,
|
||||
:root:not([data-theme="dark"]) .modal-content {
|
||||
background: #ffffff !important;
|
||||
color: #333333 !important;
|
||||
border: 1px solid #dee2e6 !important;
|
||||
}
|
||||
|
||||
[data-theme="light"] .modal-header,
|
||||
:root:not([data-theme="dark"]) .modal-header {
|
||||
border-bottom-color: #dee2e6 !important;
|
||||
}
|
||||
|
||||
[data-theme="light"] .modal-footer,
|
||||
:root:not([data-theme="dark"]) .modal-footer {
|
||||
border-top-color: #dee2e6 !important;
|
||||
}
|
||||
|
||||
[data-theme="light"] .modal-title,
|
||||
:root:not([data-theme="dark"]) .modal-title {
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
/* Tema claro - question cards */
|
||||
[data-theme="light"] .question-card-enhanced,
|
||||
:root:not([data-theme="dark"]) .question-card-enhanced {
|
||||
background: white !important;
|
||||
color: #333333 !important;
|
||||
box-shadow: 0 15px 35px rgba(0,0,0,0.1) !important;
|
||||
}
|
||||
|
||||
[data-theme="light"] .question-card,
|
||||
:root:not([data-theme="dark"]) .question-card {
|
||||
background: white !important;
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
/* Modo oscuro */
|
||||
[data-theme="dark"] {
|
||||
background: #1a1a1a !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] body {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%) !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .container-main {
|
||||
background: rgba(44, 62, 80, 0.95) !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .card {
|
||||
background: #34495e !important;
|
||||
color: #e0e0e0 !important;
|
||||
border-color: #4a5f7a !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .question-card,
|
||||
[data-theme="dark"] .question-card-enhanced {
|
||||
background: #34495e !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .option-btn,
|
||||
[data-theme="dark"] .option-enhanced {
|
||||
background: #2c3e50 !important;
|
||||
border-color: #4a5f7a !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .option-btn:hover,
|
||||
[data-theme="dark"] .option-enhanced:hover {
|
||||
background: #3d566e !important;
|
||||
border-color: var(--secondary-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .option-btn.correct,
|
||||
[data-theme="dark"] .option-enhanced.correct {
|
||||
background: rgba(39, 174, 96, 0.3) !important;
|
||||
border-color: #27ae60 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .option-btn.incorrect,
|
||||
[data-theme="dark"] .option-enhanced.incorrect {
|
||||
background: rgba(231, 76, 60, 0.3) !important;
|
||||
border-color: #e74c3c !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .option-btn.selected,
|
||||
[data-theme="dark"] .option-enhanced.selected {
|
||||
background: rgba(52, 152, 219, 0.3) !important;
|
||||
border-color: #3498db !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .navbar {
|
||||
background: rgba(26, 26, 26, 0.95) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .form-control,
|
||||
[data-theme="dark"] .form-select {
|
||||
background: #2c3e50 !important;
|
||||
border-color: #4a5f7a !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .form-control:focus,
|
||||
[data-theme="dark"] .form-select:focus {
|
||||
background: #34495e !important;
|
||||
border-color: var(--secondary-color) !important;
|
||||
color: #e0e0e0 !important;
|
||||
box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .alert-success {
|
||||
background: rgba(39, 174, 96, 0.2) !important;
|
||||
border-color: var(--success-color) !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .alert-danger {
|
||||
background: rgba(231, 76, 60, 0.2) !important;
|
||||
border-color: var(--danger-color) !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .badge {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .text-muted {
|
||||
color: #bbb !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .correct-option {
|
||||
background: rgba(39, 174, 96, 0.4) !important;
|
||||
border-color: #27ae60 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .answer-indicator {
|
||||
color: #27ae60 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .fa-check-circle {
|
||||
color: #27ae60 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .text-success {
|
||||
color: #2ecc71 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .text-danger {
|
||||
color: #e74c3c !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .text-warning {
|
||||
color: #f39c12 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .text-primary {
|
||||
color: #3498db !important;
|
||||
}
|
||||
|
||||
/* Modal support for dark theme */
|
||||
[data-theme="dark"] .modal-content {
|
||||
background: #2c3e50 !important;
|
||||
color: #e0e0e0 !important;
|
||||
border: 1px solid #4a5f7a !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-header {
|
||||
border-bottom-color: #4a5f7a !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-footer {
|
||||
border-top-color: #4a5f7a !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-title {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-close {
|
||||
filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
/* Buttons in dark theme */
|
||||
[data-theme="dark"] .btn-secondary {
|
||||
background: #4a5f7a !important;
|
||||
border-color: #4a5f7a !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-secondary:hover {
|
||||
background: #5a6f8a !important;
|
||||
border-color: #5a6f8a !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-outline-secondary {
|
||||
border-color: #4a5f7a !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-outline-secondary:hover {
|
||||
background: #4a5f7a !important;
|
||||
border-color: #4a5f7a !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-outline-primary {
|
||||
border-color: var(--secondary-color) !important;
|
||||
color: var(--secondary-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-outline-primary:hover {
|
||||
background: var(--secondary-color) !important;
|
||||
border-color: var(--secondary-color) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Progress bars in dark theme */
|
||||
[data-theme="dark"] .progress {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Badges in dark theme */
|
||||
[data-theme="dark"] .badge.bg-warning {
|
||||
background: var(--warning-color) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .badge.bg-success {
|
||||
background: var(--success-color) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .badge.bg-primary {
|
||||
background: var(--secondary-color) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Question navigator buttons in dark theme */
|
||||
[data-theme="dark"] .question-nav-btn,
|
||||
[data-theme="dark"] .nav-question-btn {
|
||||
background: #2c3e50 !important;
|
||||
border-color: #4a5f7a !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .question-nav-btn:hover,
|
||||
[data-theme="dark"] .nav-question-btn:hover {
|
||||
background: #3d566e !important;
|
||||
border-color: var(--secondary-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .question-nav-btn.btn-success,
|
||||
[data-theme="dark"] .nav-question-btn.answered {
|
||||
background: var(--success-color) !important;
|
||||
border-color: var(--success-color) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .question-nav-btn.btn-primary,
|
||||
[data-theme="dark"] .nav-question-btn.current {
|
||||
background: var(--secondary-color) !important;
|
||||
border-color: var(--secondary-color) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Timer and other exam elements */
|
||||
[data-theme="dark"] .timer-display {
|
||||
background: var(--gradient-warning) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .timer-display.warning {
|
||||
background: var(--gradient-danger) !important;
|
||||
}
|
||||
|
||||
/* Question navigator card */
|
||||
[data-theme="dark"] .question-navigator {
|
||||
background: rgba(44, 62, 80, 0.95) !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
/* Modal theme support only - no interference with Bootstrap functionality */
|
||||
|
||||
/* Modal with reasonable z-index */
|
||||
.modal {
|
||||
z-index: 1055;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
/* Custom modal styling */
|
||||
#confirmModal {
|
||||
z-index: 1055;
|
||||
}
|
||||
|
||||
#confirmBackdrop {
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
#confirmDialog {
|
||||
z-index: 1055;
|
||||
}
|
||||
|
||||
/* Prevent scroll when modal is open */
|
||||
body.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Ensure modal doesn't cause horizontal scroll */
|
||||
#confirmModal {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#confirmDialog {
|
||||
margin: 20px;
|
||||
max-width: calc(100vw - 40px);
|
||||
max-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
/* Force modal to be centered and visible */
|
||||
#confirmModal.show {
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
margin: 0 !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#confirmModal .modal-dialog {
|
||||
margin: 0 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* Ensure backdrop covers entire screen */
|
||||
.modal-backdrop.show {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
background-color: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Custom modal theme support */
|
||||
[data-theme="dark"] #confirmDialog {
|
||||
background: #2c3e50 !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #confirmDialog h5 {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #confirmDialog p {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #confirmDialog #confirm-message {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #confirmModalClose {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
/* Modo oscuro automático removido para evitar conflictos */
|
||||
|
||||
/* Efectos de carga */
|
||||
.loading-dots {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.loading-dots::after {
|
||||
content: '';
|
||||
animation: dots 1.5s steps(5, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0%, 20% { content: ''; }
|
||||
40% { content: '.'; }
|
||||
60% { content: '..'; }
|
||||
80%, 100% { content: '...'; }
|
||||
}
|
||||
|
||||
/* Confetti effect para celebrar */
|
||||
.confetti {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1040;
|
||||
}
|
||||
|
||||
.confetti-piece {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #f39c12;
|
||||
animation: confetti-fall 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes confetti-fall {
|
||||
0% {
|
||||
transform: translateY(-100vh) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) rotate(720deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
252
static/css/fontawesome-local.css
Normal file
@@ -0,0 +1,252 @@
|
||||
/* Font Awesome Local CSS */
|
||||
@font-face {
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url("../fonts/fa-solid-900.woff2") format("woff2");
|
||||
}
|
||||
|
||||
.fa,
|
||||
.fas {
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialias
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Core Font Awesome Icons */
|
||||
.fa-home:before { content: "\f015"; }
|
||||
.fa-book:before { content: "\f02d"; }
|
||||
.fa-dumbbell:before { content: "\f44b"; }
|
||||
.fa-clipboard-check:before { content: "\f46c"; }
|
||||
.fa-car:before { content: "\f1b9"; }
|
||||
.fa-moon:before { content: "\f186"; }
|
||||
.fa-sun:before { content: "\f185"; }
|
||||
.fa-check-circle:before { content: "\f058"; }
|
||||
.fa-times-circle:before { content: "\f057"; }
|
||||
.fa-arrow-left:before { content: "\f060"; }
|
||||
.fa-arrow-right:before { content: "\f061"; }
|
||||
.fa-play:before { content: "\f04b"; }
|
||||
.fa-pause:before { content: "\f04c"; }
|
||||
.fa-stop:before { content: "\f04d"; }
|
||||
.fa-refresh:before { content: "\f021"; }
|
||||
.fa-reset:before { content: "\f021"; }
|
||||
.fa-redo:before { content: "\f01e"; }
|
||||
.fa-chart-bar:before { content: "\f080"; }
|
||||
.fa-trophy:before { content: "\f091"; }
|
||||
.fa-star:before { content: "\f005"; }
|
||||
.fa-clock:before { content: "\f017"; }
|
||||
.fa-question:before { content: "\f128"; }
|
||||
.fa-info:before { content: "\f129"; }
|
||||
.fa-exclamation:before { content: "\f12a"; }
|
||||
.fa-warning:before { content: "\f071"; }
|
||||
.fa-exclamation-triangle:before { content: "\f071"; }
|
||||
.fa-check:before { content: "\f00c"; }
|
||||
.fa-times:before { content: "\f00d"; }
|
||||
.fa-plus:before { content: "\f067"; }
|
||||
.fa-minus:before { content: "\f068"; }
|
||||
.fa-edit:before { content: "\f044"; }
|
||||
.fa-trash:before { content: "\f1f8"; }
|
||||
.fa-save:before { content: "\f0c7"; }
|
||||
.fa-download:before { content: "\f019"; }
|
||||
.fa-upload:before { content: "\f093"; }
|
||||
.fa-search:before { content: "\f002"; }
|
||||
.fa-filter:before { content: "\f0b0"; }
|
||||
.fa-sort:before { content: "\f0dc"; }
|
||||
.fa-list:before { content: "\f03a"; }
|
||||
.fa-th:before { content: "\f00a"; }
|
||||
.fa-th-list:before { content: "\f00b"; }
|
||||
.fa-bars:before { content: "\f0c9"; }
|
||||
.fa-cog:before { content: "\f013"; }
|
||||
.fa-gear:before { content: "\f013"; }
|
||||
.fa-settings:before { content: "\f013"; }
|
||||
.fa-user:before { content: "\f007"; }
|
||||
.fa-users:before { content: "\f0c0"; }
|
||||
.fa-envelope:before { content: "\f0e0"; }
|
||||
.fa-phone:before { content: "\f095"; }
|
||||
.fa-calendar:before { content: "\f073"; }
|
||||
.fa-folder:before { content: "\f07b"; }
|
||||
.fa-file:before { content: "\f15b"; }
|
||||
.fa-image:before { content: "\f03e"; }
|
||||
.fa-video:before { content: "\f03d"; }
|
||||
.fa-music:before { content: "\f001"; }
|
||||
.fa-volume-up:before { content: "\f028"; }
|
||||
.fa-volume-down:before { content: "\f027"; }
|
||||
.fa-volume-off:before { content: "\f026"; }
|
||||
.fa-mute:before { content: "\f131"; }
|
||||
.fa-heart:before { content: "\f004"; }
|
||||
.fa-thumbs-up:before { content: "\f164"; }
|
||||
.fa-thumbs-down:before { content: "\f165"; }
|
||||
.fa-share:before { content: "\f064"; }
|
||||
.fa-print:before { content: "\f02f"; }
|
||||
.fa-copy:before { content: "\f0c5"; }
|
||||
.fa-cut:before { content: "\f0c4"; }
|
||||
.fa-paste:before { content: "\f0ea"; }
|
||||
.fa-undo:before { content: "\f0e2"; }
|
||||
.fa-forward:before { content: "\f04e"; }
|
||||
.fa-backward:before { content: "\f04a"; }
|
||||
.fa-step-forward:before { content: "\f051"; }
|
||||
.fa-step-backward:before { content: "\f048"; }
|
||||
.fa-fast-forward:before { content: "\f050"; }
|
||||
.fa-fast-backward:before { content: "\f049"; }
|
||||
.fa-random:before { content: "\f074"; }
|
||||
.fa-shuffle:before { content: "\f074"; }
|
||||
.fa-repeat:before { content: "\f01e"; }
|
||||
.fa-lock:before { content: "\f023"; }
|
||||
.fa-unlock:before { content: "\f09c"; }
|
||||
.fa-key:before { content: "\f084"; }
|
||||
.fa-shield:before { content: "\f132"; }
|
||||
.fa-eye:before { content: "\f06e"; }
|
||||
.fa-eye-slash:before { content: "\f070"; }
|
||||
.fa-lightbulb:before { content: "\f0eb"; }
|
||||
.fa-bell:before { content: "\f0f3"; }
|
||||
.fa-bell-slash:before { content: "\f1f6"; }
|
||||
.fa-flag:before { content: "\f024"; }
|
||||
.fa-bookmark:before { content: "\f02e"; }
|
||||
.fa-tag:before { content: "\f02b"; }
|
||||
.fa-tags:before { content: "\f02c"; }
|
||||
.fa-comment:before { content: "\f075"; }
|
||||
.fa-comments:before { content: "\f086"; }
|
||||
.fa-quote-left:before { content: "\f10d"; }
|
||||
.fa-quote-right:before { content: "\f10e"; }
|
||||
.fa-link:before { content: "\f0c1"; }
|
||||
.fa-unlink:before { content: "\f127"; }
|
||||
.fa-external-link:before { content: "\f08e"; }
|
||||
.fa-external-link-alt:before { content: "\f35d"; }
|
||||
.fa-anchor:before { content: "\f13d"; }
|
||||
.fa-globe:before { content: "\f0ac"; }
|
||||
.fa-language:before { content: "\f1ab"; }
|
||||
.fa-map:before { content: "\f279"; }
|
||||
.fa-map-marker:before { content: "\f041"; }
|
||||
.fa-location:before { content: "\f041"; }
|
||||
.fa-compass:before { content: "\f14e"; }
|
||||
.fa-road:before { content: "\f018"; }
|
||||
.fa-plane:before { content: "\f072"; }
|
||||
.fa-train:before { content: "\f238"; }
|
||||
.fa-bus:before { content: "\f207"; }
|
||||
.fa-taxi:before { content: "\f1ba"; }
|
||||
.fa-bicycle:before { content: "\f206"; }
|
||||
.fa-motorcycle:before { content: "\f21c"; }
|
||||
.fa-truck:before { content: "\f0d1"; }
|
||||
.fa-ship:before { content: "\f21a"; }
|
||||
.fa-rocket:before { content: "\f135"; }
|
||||
.fa-fire:before { content: "\f06d"; }
|
||||
.fa-bolt:before { content: "\f0e7"; }
|
||||
.fa-lightning:before { content: "\f0e7"; }
|
||||
.fa-magic:before { content: "\f0d0"; }
|
||||
.fa-wand:before { content: "\f0d0"; }
|
||||
.fa-gift:before { content: "\f06b"; }
|
||||
.fa-birthday-cake:before { content: "\f1fd"; }
|
||||
.fa-cake:before { content: "\f1fd"; }
|
||||
.fa-coffee:before { content: "\f0f4"; }
|
||||
.fa-beer:before { content: "\f0fc"; }
|
||||
.fa-wine:before { content: "\f72f"; }
|
||||
.fa-utensils:before { content: "\f2e7"; }
|
||||
.fa-cutlery:before { content: "\f0f5"; }
|
||||
.fa-shopping-cart:before { content: "\f07a"; }
|
||||
.fa-credit-card:before { content: "\f09d"; }
|
||||
.fa-money:before { content: "\f0d6"; }
|
||||
.fa-dollar:before { content: "\f155"; }
|
||||
.fa-euro:before { content: "\f153"; }
|
||||
.fa-pound:before { content: "\f154"; }
|
||||
.fa-yen:before { content: "\f157"; }
|
||||
.fa-bitcoin:before { content: "\f379"; }
|
||||
.fa-bank:before { content: "\f19c"; }
|
||||
.fa-building:before { content: "\f1ad"; }
|
||||
.fa-hospital:before { content: "\f0f8"; }
|
||||
.fa-school:before { content: "\f549"; }
|
||||
.fa-university:before { content: "\f19c"; }
|
||||
.fa-graduation-cap:before { content: "\f19d"; }
|
||||
.fa-briefcase:before { content: "\f0b1"; }
|
||||
.fa-suitcase:before { content: "\f0f2"; }
|
||||
.fa-laptop:before { content: "\f109"; }
|
||||
.fa-desktop:before { content: "\f108"; }
|
||||
.fa-tablet:before { content: "\f10a"; }
|
||||
.fa-mobile:before { content: "\f10b"; }
|
||||
.fa-phone-alt:before { content: "\f879"; }
|
||||
.fa-keyboard:before { content: "\f11c"; }
|
||||
.fa-mouse:before { content: "\f8cc"; }
|
||||
.fa-gamepad:before { content: "\f11b"; }
|
||||
.fa-tv:before { content: "\f26c"; }
|
||||
.fa-camera:before { content: "\f030"; }
|
||||
.fa-microphone:before { content: "\f130"; }
|
||||
.fa-headphones:before { content: "\f025"; }
|
||||
.fa-speaker:before { content: "\f028"; }
|
||||
.fa-battery-full:before { content: "\f240"; }
|
||||
.fa-battery-half:before { content: "\f242"; }
|
||||
.fa-battery-empty:before { content: "\f244"; }
|
||||
.fa-plug:before { content: "\f1e6"; }
|
||||
.fa-wifi:before { content: "\f1eb"; }
|
||||
.fa-signal:before { content: "\f012"; }
|
||||
.fa-bluetooth:before { content: "\f293"; }
|
||||
.fa-usb:before { content: "\f287"; }
|
||||
.fa-cloud:before { content: "\f0c2"; }
|
||||
.fa-cloud-upload:before { content: "\f0ee"; }
|
||||
.fa-cloud-download:before { content: "\f0ed"; }
|
||||
.fa-database:before { content: "\f1c0"; }
|
||||
.fa-server:before { content: "\f233"; }
|
||||
.fa-code:before { content: "\f121"; }
|
||||
.fa-terminal:before { content: "\f120"; }
|
||||
.fa-bug:before { content: "\f188"; }
|
||||
.fa-wrench:before { content: "\f0ad"; }
|
||||
.fa-hammer:before { content: "\f6e3"; }
|
||||
.fa-screwdriver:before { content: "\f54a"; }
|
||||
.fa-tools:before { content: "\f7d9"; }
|
||||
.fa-paint-brush:before { content: "\f1fc"; }
|
||||
.fa-palette:before { content: "\f53f"; }
|
||||
.fa-ruler:before { content: "\f545"; }
|
||||
.fa-calculator:before { content: "\f1ec"; }
|
||||
.fa-balance-scale:before { content: "\f24e"; }
|
||||
.fa-thermometer:before { content: "\f491"; }
|
||||
.fa-stopwatch:before { content: "\f2f2"; }
|
||||
.fa-timer:before { content: "\f2f2"; }
|
||||
.fa-hourglass:before { content: "\f254"; }
|
||||
.fa-history:before { content: "\f1da"; }
|
||||
.fa-calendar-alt:before { content: "\f073"; }
|
||||
.fa-calendar-check:before { content: "\f274"; }
|
||||
.fa-calendar-times:before { content: "\f273"; }
|
||||
.fa-calendar-plus:before { content: "\f271"; }
|
||||
.fa-calendar-minus:before { content: "\f272"; }
|
||||
.fa-sticky-note:before { content: "\f249"; }
|
||||
.fa-paperclip:before { content: "\f0c6"; }
|
||||
.fa-pushpin:before { content: "\f08d"; }
|
||||
.fa-thumbtack:before { content: "\f08d"; }
|
||||
.fa-eraser:before { content: "\f12d"; }
|
||||
.fa-pen:before { content: "\f304"; }
|
||||
.fa-pencil:before { content: "\f040"; }
|
||||
.fa-marker:before { content: "\f5a1"; }
|
||||
.fa-highlighter:before { content: "\f591"; }
|
||||
.fa-ruler-combined:before { content: "\f546"; }
|
||||
.fa-compass-drafting:before { content: "\f568"; }
|
||||
.fa-drafting-compass:before { content: "\f568"; }
|
||||
.fa-square:before { content: "\f0c8"; }
|
||||
.fa-circle:before { content: "\f111"; }
|
||||
.fa-triangle:before { content: "\f2ec"; }
|
||||
.fa-polygon:before { content: "\f2ec"; }
|
||||
.fa-shapes:before { content: "\f61f"; }
|
||||
.fa-cube:before { content: "\f1b2"; }
|
||||
.fa-cubes:before { content: "\f1b3"; }
|
||||
.fa-dice:before { content: "\f522"; }
|
||||
.fa-dice-one:before { content: "\f525"; }
|
||||
.fa-dice-two:before { content: "\f528"; }
|
||||
.fa-dice-three:before { content: "\f527"; }
|
||||
.fa-dice-four:before { content: "\f524"; }
|
||||
.fa-dice-five:before { content: "\f523"; }
|
||||
.fa-dice-six:before { content: "\f526"; }
|
||||
.fa-chess:before { content: "\f439"; }
|
||||
.fa-chess-king:before { content: "\f43f"; }
|
||||
.fa-chess-queen:before { content: "\f445"; }
|
||||
.fa-chess-rook:before { content: "\f447"; }
|
||||
.fa-chess-bishop:before { content: "\f43a"; }
|
||||
.fa-chess-knight:before { content: "\f441"; }
|
||||
.fa-chess-pawn:before { content: "\f443"; }
|
||||
.fa-puzzle-piece:before { content: "\f12e"; }
|
||||
.fa-jigsaw:before { content: "\f12e"; }
|
||||
.fa-gamepad2:before { content: "\f11b"; }
|
||||
.fa-joystick:before { content: "\f11b"; }
|
||||
.fa-controller:before { content: "\f11b"; }
|
||||
14
static/favicon.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3498db"/>
|
||||
<stop offset="100%" style="stop-color:#2c3e50"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<circle cx="50" cy="50" r="48" fill="url(#bg)"/>
|
||||
|
||||
<!-- Car emoji -->
|
||||
<text x="50" y="65" text-anchor="middle" font-size="45" fill="#fff">🚗</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 466 B |
BIN
static/fonts/fa-solid-900.woff2
Normal file
BIN
static/images/img100.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img101.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img102.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img103.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img104.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img105.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
static/images/img106.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
static/images/img107.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/images/img108.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
static/images/img109.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img110.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
static/images/img111.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
static/images/img112.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
static/images/img113.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img114.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
static/images/img115.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img116.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img117.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
static/images/img118.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img119.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img120.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img121.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/images/img122.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img123.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img124.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img125.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img126.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img127.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img128.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img129.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img130.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/images/img131.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img132.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/images/img133.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img134.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/images/img135.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
static/images/img136.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/images/img137.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img138.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/images/img139.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
static/images/img140.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
static/images/img141.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
static/images/img142.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
static/images/img143.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
static/images/img144.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
static/images/img145.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
static/images/img146.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
static/images/img147.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img148.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
static/images/img151.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
static/images/img18.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/images/img181.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img182.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img187.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
static/images/img197.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
static/images/img199.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
static/images/img200.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img28.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/images/img32.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
static/images/img33.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/images/img35.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
static/images/img4.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img50.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img67.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/images/img9.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/img93.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
static/images/img99.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
338
static/js/app.js
Normal file
@@ -0,0 +1,338 @@
|
||||
// Funciones globales para la aplicación del Balotario
|
||||
|
||||
// Configuración global
|
||||
const APP_CONFIG = {
|
||||
ANIMATION_DURATION: 300,
|
||||
CONFETTI_DURATION: 3000,
|
||||
SOUND_ENABLED: true,
|
||||
DARK_MODE: false
|
||||
};
|
||||
|
||||
// Utilidades generales
|
||||
class Utils {
|
||||
static showNotification(message, type = 'info', duration = 3000) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||||
notification.style.cssText = `
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
min-width: 300px;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, duration);
|
||||
}
|
||||
|
||||
static playSound(type) {
|
||||
if (!APP_CONFIG.SOUND_ENABLED) return;
|
||||
|
||||
// Crear sonidos usando Web Audio API
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
const frequencies = {
|
||||
correct: 800,
|
||||
incorrect: 300,
|
||||
click: 600,
|
||||
complete: 1000
|
||||
};
|
||||
|
||||
oscillator.frequency.setValueAtTime(frequencies[type] || 600, audioContext.currentTime);
|
||||
oscillator.type = 'sine';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.1);
|
||||
}
|
||||
|
||||
static createConfetti() {
|
||||
const confettiContainer = document.createElement('div');
|
||||
confettiContainer.className = 'confetti';
|
||||
document.body.appendChild(confettiContainer);
|
||||
|
||||
const colors = ['#f39c12', '#e74c3c', '#3498db', '#27ae60', '#9b59b6'];
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const confettiPiece = document.createElement('div');
|
||||
confettiPiece.className = 'confetti-piece';
|
||||
confettiPiece.style.cssText = `
|
||||
left: ${Math.random() * 100}%;
|
||||
background: ${colors[Math.floor(Math.random() * colors.length)]};
|
||||
animation-delay: ${Math.random() * 3}s;
|
||||
animation-duration: ${3 + Math.random() * 2}s;
|
||||
`;
|
||||
confettiContainer.appendChild(confettiPiece);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
confettiContainer.remove();
|
||||
}, APP_CONFIG.CONFETTI_DURATION);
|
||||
}
|
||||
|
||||
static formatTime(seconds) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
static saveToLocalStorage(key, data) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.warn('No se pudo guardar en localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
static loadFromLocalStorage(key, defaultValue = null) {
|
||||
try {
|
||||
const data = localStorage.getItem(key);
|
||||
return data ? JSON.parse(data) : defaultValue;
|
||||
} catch (e) {
|
||||
console.warn('No se pudo cargar de localStorage:', e);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clase para manejar estadísticas
|
||||
class StatsManager {
|
||||
constructor() {
|
||||
this.stats = Utils.loadFromLocalStorage('balotario_stats', {
|
||||
totalAnswered: 0,
|
||||
correctAnswers: 0,
|
||||
incorrectAnswers: 0,
|
||||
accuracy: 0,
|
||||
studyTime: 0,
|
||||
examsTaken: 0,
|
||||
bestScore: 0,
|
||||
streak: 0,
|
||||
lastActivity: null
|
||||
});
|
||||
}
|
||||
|
||||
updateStats(isCorrect) {
|
||||
this.stats.totalAnswered++;
|
||||
if (isCorrect) {
|
||||
this.stats.correctAnswers++;
|
||||
this.stats.streak++;
|
||||
} else {
|
||||
this.stats.incorrectAnswers++;
|
||||
this.stats.streak = 0;
|
||||
}
|
||||
|
||||
this.stats.accuracy = this.stats.totalAnswered > 0 ?
|
||||
Math.round((this.stats.correctAnswers / this.stats.totalAnswered) * 100) : 0;
|
||||
|
||||
this.stats.lastActivity = new Date().toISOString();
|
||||
this.saveStats();
|
||||
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
updateExamScore(score) {
|
||||
this.stats.examsTaken++;
|
||||
if (score > this.stats.bestScore) {
|
||||
this.stats.bestScore = score;
|
||||
}
|
||||
this.saveStats();
|
||||
}
|
||||
|
||||
addStudyTime(minutes) {
|
||||
this.stats.studyTime += minutes;
|
||||
this.saveStats();
|
||||
}
|
||||
|
||||
getStats() {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
saveStats() {
|
||||
Utils.saveToLocalStorage('balotario_stats', this.stats);
|
||||
}
|
||||
|
||||
resetStats() {
|
||||
this.stats = {
|
||||
totalAnswered: 0,
|
||||
correctAnswers: 0,
|
||||
incorrectAnswers: 0,
|
||||
accuracy: 0,
|
||||
studyTime: 0,
|
||||
examsTaken: 0,
|
||||
bestScore: 0,
|
||||
streak: 0,
|
||||
lastActivity: null
|
||||
};
|
||||
this.saveStats();
|
||||
}
|
||||
}
|
||||
|
||||
// Clase para manejar el progreso circular
|
||||
class CircularProgress {
|
||||
constructor(element, options = {}) {
|
||||
this.element = element;
|
||||
this.options = {
|
||||
size: 120,
|
||||
strokeWidth: 8,
|
||||
color: '#3498db',
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
...options
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
const { size, strokeWidth } = this.options;
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = radius * 2 * Math.PI;
|
||||
|
||||
this.element.innerHTML = `
|
||||
<svg width="${size}" height="${size}" class="progress-ring">
|
||||
<circle class="bg" cx="${size/2}" cy="${size/2}" r="${radius}"
|
||||
stroke="${this.options.backgroundColor}" stroke-width="${strokeWidth}"/>
|
||||
<circle class="progress" cx="${size/2}" cy="${size/2}" r="${radius}"
|
||||
stroke="${this.options.color}" stroke-width="${strokeWidth}"
|
||||
stroke-dasharray="${circumference}" stroke-dashoffset="${circumference}"/>
|
||||
</svg>
|
||||
<div class="position-absolute top-50 start-50 translate-middle text-center">
|
||||
<div class="progress-text fw-bold"></div>
|
||||
<div class="progress-label small"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.progressCircle = this.element.querySelector('.progress');
|
||||
this.progressText = this.element.querySelector('.progress-text');
|
||||
this.progressLabel = this.element.querySelector('.progress-label');
|
||||
this.circumference = circumference;
|
||||
}
|
||||
|
||||
setProgress(percentage, text = '', label = '') {
|
||||
const offset = this.circumference - (percentage / 100) * this.circumference;
|
||||
this.progressCircle.style.strokeDashoffset = offset;
|
||||
this.progressText.textContent = text || `${percentage}%`;
|
||||
this.progressLabel.textContent = label;
|
||||
}
|
||||
}
|
||||
|
||||
// Clase para manejar temas y preferencias
|
||||
class ThemeManager {
|
||||
constructor() {
|
||||
this.currentTheme = Utils.loadFromLocalStorage('theme', 'light');
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
toggleTheme() {
|
||||
this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
|
||||
this.applyTheme();
|
||||
Utils.saveToLocalStorage('theme', this.currentTheme);
|
||||
}
|
||||
|
||||
applyTheme() {
|
||||
document.documentElement.setAttribute('data-theme', this.currentTheme);
|
||||
document.body.setAttribute('data-theme', this.currentTheme);
|
||||
APP_CONFIG.DARK_MODE = this.currentTheme === 'dark';
|
||||
|
||||
// Forzar actualización de estilos
|
||||
document.body.style.display = 'none';
|
||||
document.body.offsetHeight; // Trigger reflow
|
||||
document.body.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Clase para manejar atajos de teclado
|
||||
class KeyboardManager {
|
||||
constructor() {
|
||||
this.shortcuts = new Map();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const key = this.getKeyString(e);
|
||||
if (this.shortcuts.has(key)) {
|
||||
e.preventDefault();
|
||||
this.shortcuts.get(key)();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getKeyString(event) {
|
||||
const parts = [];
|
||||
if (event.ctrlKey) parts.push('ctrl');
|
||||
if (event.altKey) parts.push('alt');
|
||||
if (event.shiftKey) parts.push('shift');
|
||||
parts.push(event.key.toLowerCase());
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
addShortcut(keys, callback) {
|
||||
this.shortcuts.set(keys, callback);
|
||||
}
|
||||
|
||||
removeShortcut(keys) {
|
||||
this.shortcuts.delete(keys);
|
||||
}
|
||||
}
|
||||
|
||||
// Inicialización global
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Inicializar managers globales
|
||||
window.statsManager = new StatsManager();
|
||||
window.themeManager = new ThemeManager();
|
||||
window.keyboardManager = new KeyboardManager();
|
||||
|
||||
// Agregar atajos de teclado globales
|
||||
window.keyboardManager.addShortcut('ctrl+d', () => {
|
||||
window.themeManager.toggleTheme();
|
||||
Utils.showNotification('Tema cambiado', 'info');
|
||||
});
|
||||
|
||||
// Mejorar la experiencia de navegación
|
||||
const links = document.querySelectorAll('a[href^="/"]');
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
Utils.playSound('click');
|
||||
});
|
||||
});
|
||||
|
||||
// Agregar efectos de hover a las tarjetas
|
||||
const cards = document.querySelectorAll('.card');
|
||||
cards.forEach(card => {
|
||||
card.classList.add('card-hover');
|
||||
});
|
||||
|
||||
// Inicializar tooltips de Bootstrap
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Mostrar mensaje de bienvenida
|
||||
const isFirstVisit = !Utils.loadFromLocalStorage('hasVisited', false);
|
||||
if (isFirstVisit) {
|
||||
setTimeout(() => {
|
||||
Utils.showNotification('¡Bienvenido al Balotario! Usa Ctrl+D para cambiar el tema.', 'info', 5000);
|
||||
Utils.saveToLocalStorage('hasVisited', true);
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// Exportar para uso global
|
||||
window.Utils = Utils;
|
||||
window.StatsManager = StatsManager;
|
||||
window.CircularProgress = CircularProgress;
|
||||
139
static/js/modules/base.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// Base module for common functionality
|
||||
class BaseModule {
|
||||
constructor() {
|
||||
this.questions = [];
|
||||
this.currentIndex = 0;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
// Common DOM utilities
|
||||
static getElementById(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
static show(element) {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
if (element) element.style.display = 'block';
|
||||
}
|
||||
|
||||
static hide(element) {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
if (element) element.style.display = 'none';
|
||||
}
|
||||
|
||||
static showFlex(element) {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
if (element) element.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Common API calls
|
||||
async fetchQuestions(params = {}) {
|
||||
const url = new URL('/api/questions', window.location.origin);
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] !== undefined && params[key] !== null) {
|
||||
url.searchParams.append(key, params[key]);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching questions:', error);
|
||||
this.showNotification('Error al cargar las preguntas', 'danger');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Common notification wrapper
|
||||
showNotification(message, type = 'info', duration = 3000) {
|
||||
if (window.Utils) {
|
||||
Utils.showNotification(message, type, duration);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Common sound wrapper
|
||||
playSound(type) {
|
||||
if (window.Utils) {
|
||||
Utils.playSound(type);
|
||||
}
|
||||
}
|
||||
|
||||
// Common loading states
|
||||
showLoading(loadingId = 'loading') {
|
||||
BaseModule.showFlex(loadingId);
|
||||
}
|
||||
|
||||
hideLoading(loadingId = 'loading') {
|
||||
BaseModule.hide(loadingId);
|
||||
}
|
||||
|
||||
// Common question display utilities
|
||||
createQuestionHTML(question, currentIndex, totalQuestions) {
|
||||
let html = `
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0">Pregunta ${question.id}</h5>
|
||||
<span class="badge bg-primary">${currentIndex + 1} de ${totalQuestions}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (question.has_image && question.image) {
|
||||
html += `<img src="${question.image}" class="question-image" alt="Imagen de la pregunta">`;
|
||||
}
|
||||
|
||||
html += `<p class="lead mb-4">${question.question}</p>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
// Common option creation
|
||||
createOptionsHTML(options, correctAnswer, optionClass = 'option-btn') {
|
||||
let html = '<div class="options-container">';
|
||||
options.forEach((option, index) => {
|
||||
const letter = String.fromCharCode(97 + index); // a, b, c, d
|
||||
const isCorrect = letter === correctAnswer;
|
||||
html += `
|
||||
<div class="${optionClass} mb-2 p-3 border rounded"
|
||||
data-correct="${isCorrect}" data-letter="${letter}">
|
||||
<strong>${option}</strong>
|
||||
<i class="fas fa-check-circle text-success ms-2 answer-indicator" style="display: none;"></i>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// Common navigation update
|
||||
updateNavigation(prevBtnId, nextBtnId, currentQuestionId) {
|
||||
const prevBtn = BaseModule.getElementById(prevBtnId);
|
||||
const nextBtn = BaseModule.getElementById(nextBtnId);
|
||||
const currentQuestionEl = BaseModule.getElementById(currentQuestionId);
|
||||
|
||||
if (prevBtn) prevBtn.disabled = this.currentIndex === 0;
|
||||
if (nextBtn) nextBtn.disabled = this.currentIndex === this.questions.length - 1;
|
||||
if (currentQuestionEl) currentQuestionEl.textContent = this.currentIndex + 1;
|
||||
}
|
||||
|
||||
// Common progress update
|
||||
updateProgress(progressBarId) {
|
||||
const progressBar = BaseModule.getElementById(progressBarId);
|
||||
if (progressBar && this.questions.length > 0) {
|
||||
const progress = ((this.currentIndex + 1) / this.questions.length) * 100;
|
||||
progressBar.style.width = progress + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for global use
|
||||
window.BaseModule = BaseModule;
|
||||
7
static/js/modules/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
1
static/js/modules/bootstrap/bootstrap.bundle.min.js.map
Normal file
600
static/js/modules/exam-mode.js
Normal file
@@ -0,0 +1,600 @@
|
||||
// Exam Mode Module
|
||||
class ExamMode extends BaseModule {
|
||||
constructor() {
|
||||
super();
|
||||
this.examAnswers = {};
|
||||
this.examTimer = null;
|
||||
this.examTimeLimit = 30; // minutes
|
||||
this.examStartTime = null;
|
||||
this.navigator = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.getElementById('start-exam')?.addEventListener('click', () => {
|
||||
this.startExam();
|
||||
});
|
||||
|
||||
document.getElementById('prev-exam-btn')?.addEventListener('click', () => {
|
||||
this.previousQuestion();
|
||||
});
|
||||
|
||||
document.getElementById('next-exam-btn')?.addEventListener('click', () => {
|
||||
this.nextQuestion();
|
||||
});
|
||||
|
||||
document.getElementById('finish-exam')?.addEventListener('click', () => {
|
||||
this.showFinishConfirmation();
|
||||
});
|
||||
|
||||
document.getElementById('retake-exam')?.addEventListener('click', () => {
|
||||
this.resetExam();
|
||||
});
|
||||
|
||||
document.getElementById('review-answers')?.addEventListener('click', () => {
|
||||
this.showReviewMode();
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
this.handleKeyboard(e);
|
||||
});
|
||||
|
||||
// Modal event listeners
|
||||
this.setupModalListeners();
|
||||
}
|
||||
|
||||
setupModalListeners() {
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'confirm-finish') {
|
||||
this.finishExam();
|
||||
this.hideConfirmModal();
|
||||
} else if (e.target.id === 'confirmModalCancel' || e.target.id === 'confirmModalClose') {
|
||||
this.hideConfirmModal();
|
||||
} else if (e.target.id === 'confirmBackdrop') {
|
||||
this.hideConfirmModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async startExam() {
|
||||
const questionCount = parseInt(document.getElementById('exam-questions').value);
|
||||
this.examTimeLimit = parseInt(document.getElementById('exam-time').value);
|
||||
|
||||
try {
|
||||
const data = await this.fetchQuestions({
|
||||
mode: 'random',
|
||||
count: questionCount
|
||||
});
|
||||
|
||||
this.questions = data;
|
||||
this.examAnswers = {};
|
||||
this.currentIndex = 0;
|
||||
this.examStartTime = new Date();
|
||||
|
||||
this.initializeNavigator();
|
||||
this.displayQuestion();
|
||||
this.startTimer();
|
||||
|
||||
BaseModule.hide('exam-setup');
|
||||
BaseModule.show('exam-content');
|
||||
|
||||
this.updateExamProgress();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error starting exam:', error);
|
||||
}
|
||||
}
|
||||
|
||||
initializeNavigator() {
|
||||
if (!this.navigator) {
|
||||
this.navigator = new QuestionNavigator('question-navigator', {
|
||||
onQuestionSelect: (index) => {
|
||||
this.currentIndex = index;
|
||||
this.displayQuestion();
|
||||
},
|
||||
storagePrefix: 'exam_temp' // Don't persist exam navigation
|
||||
});
|
||||
}
|
||||
|
||||
this.navigator.setQuestions(this.questions);
|
||||
}
|
||||
|
||||
displayQuestion() {
|
||||
if (this.currentIndex >= this.questions.length) return;
|
||||
|
||||
const question = this.questions[this.currentIndex];
|
||||
|
||||
let html = this.createQuestionHTML(question, this.currentIndex, this.questions.length);
|
||||
html += this.createExamOptionsHTML(question.options);
|
||||
|
||||
document.getElementById('exam-question-container').innerHTML = html;
|
||||
document.getElementById('question-progress').textContent = `${this.currentIndex + 1} de ${this.questions.length}`;
|
||||
|
||||
// Setup option click handlers
|
||||
this.setupExamOptionHandlers(question);
|
||||
|
||||
this.updateExamNavigation();
|
||||
if (this.navigator) {
|
||||
this.navigator.setCurrentIndex(this.currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
setupExamOptionHandlers(question) {
|
||||
const optionButtons = document.querySelectorAll('.option-enhanced');
|
||||
optionButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
this.playSound('click');
|
||||
optionButtons.forEach(btn => btn.classList.remove('selected'));
|
||||
button.classList.add('selected');
|
||||
|
||||
const selectedAnswer = button.getAttribute('data-answer');
|
||||
this.examAnswers[question.id] = selectedAnswer;
|
||||
|
||||
if (this.navigator) {
|
||||
this.navigator.updateNavigator();
|
||||
}
|
||||
this.updateExamProgress();
|
||||
});
|
||||
});
|
||||
|
||||
// Pre-select if already answered
|
||||
if (this.examAnswers[question.id]) {
|
||||
const selectedButton = document.querySelector(`[data-answer="${this.examAnswers[question.id]}"]`);
|
||||
if (selectedButton) {
|
||||
selectedButton.classList.add('selected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousQuestion() {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
this.displayQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
nextQuestion() {
|
||||
if (this.currentIndex < this.questions.length - 1) {
|
||||
this.currentIndex++;
|
||||
this.displayQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
updateExamNavigation() {
|
||||
document.getElementById('prev-exam-btn').disabled = this.currentIndex === 0;
|
||||
document.getElementById('next-exam-btn').disabled = this.currentIndex === this.questions.length - 1;
|
||||
}
|
||||
|
||||
updateExamProgress() {
|
||||
const answeredCount = Object.keys(this.examAnswers).length;
|
||||
const remainingCount = this.questions.length - answeredCount;
|
||||
|
||||
document.getElementById('answered-count').textContent = answeredCount;
|
||||
document.getElementById('remaining-count').textContent = remainingCount;
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
const endTime = new Date(this.examStartTime.getTime() + this.examTimeLimit * 60000);
|
||||
|
||||
this.examTimer = setInterval(() => {
|
||||
const now = new Date();
|
||||
const timeLeft = endTime - now;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(this.examTimer);
|
||||
this.finishExam();
|
||||
return;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(timeLeft / 60000);
|
||||
const seconds = Math.floor((timeLeft % 60000) / 1000);
|
||||
|
||||
document.getElementById('timer').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
// Change color when time is running out
|
||||
const timerElement = document.getElementById('timer');
|
||||
if (minutes < 5) {
|
||||
timerElement.classList.remove('text-warning');
|
||||
timerElement.classList.add('text-danger');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
showFinishConfirmation() {
|
||||
const unansweredCount = this.questions.length - Object.keys(this.examAnswers).length;
|
||||
document.getElementById('unanswered-warning').textContent = unansweredCount;
|
||||
|
||||
document.getElementById('confirmModal').style.display = 'block';
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
hideConfirmModal() {
|
||||
document.getElementById('confirmModal').style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
|
||||
finishExam() {
|
||||
clearInterval(this.examTimer);
|
||||
|
||||
let correct = 0;
|
||||
let incorrect = 0;
|
||||
let unanswered = 0;
|
||||
|
||||
this.questions.forEach(question => {
|
||||
if (this.examAnswers.hasOwnProperty(question.id)) {
|
||||
if (this.examAnswers[question.id] === question.correct) {
|
||||
correct++;
|
||||
} else {
|
||||
incorrect++;
|
||||
}
|
||||
} else {
|
||||
unanswered++;
|
||||
}
|
||||
});
|
||||
|
||||
const score = Math.round((correct / this.questions.length) * 100);
|
||||
const passed = score >= 70; // 70% to pass
|
||||
|
||||
// Update global stats
|
||||
if (window.statsManager) {
|
||||
for (let i = 0; i < correct; i++) {
|
||||
window.statsManager.updateStats(true);
|
||||
}
|
||||
for (let i = 0; i < incorrect; i++) {
|
||||
window.statsManager.updateStats(false);
|
||||
}
|
||||
window.statsManager.updateExamScore(score);
|
||||
}
|
||||
|
||||
// Show results
|
||||
this.showExamResults(correct, incorrect, unanswered, score, passed);
|
||||
}
|
||||
|
||||
showExamResults(correct, incorrect, unanswered, score, passed) {
|
||||
document.getElementById('exam-correct').textContent = correct;
|
||||
document.getElementById('exam-incorrect').textContent = incorrect;
|
||||
document.getElementById('exam-unanswered').textContent = unanswered;
|
||||
document.getElementById('exam-score').textContent = score + '%';
|
||||
|
||||
const progressBar = document.getElementById('exam-progress-bar');
|
||||
progressBar.style.width = score + '%';
|
||||
|
||||
const resultIcon = document.getElementById('result-icon');
|
||||
const resultTitle = document.getElementById('result-title');
|
||||
const passMessage = document.getElementById('pass-message');
|
||||
|
||||
if (passed) {
|
||||
progressBar.classList.remove('bg-danger');
|
||||
progressBar.classList.add('bg-success');
|
||||
resultIcon.innerHTML = '<i class="fas fa-trophy fa-4x text-success"></i>';
|
||||
resultTitle.textContent = '¡Felicitaciones! Has Aprobado';
|
||||
passMessage.classList.add('alert-success');
|
||||
passMessage.textContent = 'Has obtenido una calificación aprobatoria. ¡Buen trabajo!';
|
||||
passMessage.style.display = 'block';
|
||||
|
||||
if (window.Utils) {
|
||||
Utils.createConfetti();
|
||||
Utils.playSound('complete');
|
||||
}
|
||||
} else {
|
||||
progressBar.classList.remove('bg-success');
|
||||
progressBar.classList.add('bg-danger');
|
||||
resultIcon.innerHTML = '<i class="fas fa-times-circle fa-4x text-danger"></i>';
|
||||
resultTitle.textContent = 'Examen No Aprobado';
|
||||
passMessage.classList.add('alert-danger');
|
||||
passMessage.textContent = 'Necesitas al menos 70% para aprobar. ¡Sigue estudiando y vuelve a intentarlo!';
|
||||
passMessage.style.display = 'block';
|
||||
}
|
||||
|
||||
BaseModule.hide('exam-content');
|
||||
BaseModule.show('exam-results');
|
||||
}
|
||||
|
||||
resetExam() {
|
||||
this.examAnswers = {};
|
||||
this.currentIndex = 0;
|
||||
clearInterval(this.examTimer);
|
||||
|
||||
BaseModule.hide('exam-results');
|
||||
BaseModule.show('exam-setup');
|
||||
}
|
||||
|
||||
createExamOptionsHTML(options) {
|
||||
let html = '<div class="options-container">';
|
||||
options.forEach((option, index) => {
|
||||
const letter = String.fromCharCode(97 + index); // a, b, c, d
|
||||
html += `
|
||||
<button class="option-enhanced" data-answer="${letter}">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="badge bg-primary me-3">${letter.toUpperCase()}</span>
|
||||
<span>${option.replace(/^[a-d]\)\s*/, '')}</span>
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
showReviewMode() {
|
||||
this.reviewIndex = 0;
|
||||
this.setupReviewEventListeners();
|
||||
this.displayReviewQuestion();
|
||||
this.createReviewNavigator();
|
||||
|
||||
BaseModule.hide('exam-results');
|
||||
BaseModule.show('exam-review');
|
||||
}
|
||||
|
||||
setupReviewEventListeners() {
|
||||
// Remove existing listeners to avoid duplicates
|
||||
document.getElementById('prev-review-btn')?.removeEventListener('click', this.prevReviewHandler);
|
||||
document.getElementById('next-review-btn')?.removeEventListener('click', this.nextReviewHandler);
|
||||
document.getElementById('back-to-results')?.removeEventListener('click', this.backToResultsHandler);
|
||||
|
||||
// Create bound handlers
|
||||
this.prevReviewHandler = () => this.previousReviewQuestion();
|
||||
this.nextReviewHandler = () => this.nextReviewQuestion();
|
||||
this.backToResultsHandler = () => this.backToResults();
|
||||
|
||||
// Add event listeners
|
||||
document.getElementById('prev-review-btn')?.addEventListener('click', this.prevReviewHandler);
|
||||
document.getElementById('next-review-btn')?.addEventListener('click', this.nextReviewHandler);
|
||||
document.getElementById('back-to-results')?.addEventListener('click', this.backToResultsHandler);
|
||||
}
|
||||
|
||||
displayReviewQuestion() {
|
||||
if (this.reviewIndex >= this.questions.length) return;
|
||||
|
||||
const question = this.questions[this.reviewIndex];
|
||||
const userAnswer = this.examAnswers[question.id];
|
||||
const isCorrect = userAnswer === question.correct;
|
||||
const wasAnswered = userAnswer !== undefined;
|
||||
|
||||
let html = `
|
||||
<div class="question-card-enhanced fade-in">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0 text-gradient">Pregunta ${question.id}</h5>
|
||||
<div>
|
||||
<span class="badge bg-primary me-2">${this.reviewIndex + 1} de ${this.questions.length}</span>
|
||||
${wasAnswered ?
|
||||
(isCorrect ?
|
||||
'<span class="badge bg-success"><i class="fas fa-check me-1"></i>Correcta</span>' :
|
||||
'<span class="badge bg-danger"><i class="fas fa-times me-1"></i>Incorrecta</span>'
|
||||
) :
|
||||
'<span class="badge bg-secondary"><i class="fas fa-question me-1"></i>Sin responder</span>'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (question.has_image && question.image) {
|
||||
html += `<img src="${question.image}" class="question-image" alt="Imagen de la pregunta">`;
|
||||
}
|
||||
|
||||
html += `<p class="lead mb-4">${question.question}</p>`;
|
||||
|
||||
html += '<div class="options-container">';
|
||||
question.options.forEach((option, index) => {
|
||||
const letter = String.fromCharCode(97 + index); // a, b, c, d
|
||||
const isUserAnswer = letter === userAnswer;
|
||||
const isCorrectAnswer = letter === question.correct;
|
||||
|
||||
let optionClass = 'option-enhanced';
|
||||
let badgeClass = 'bg-primary';
|
||||
let iconHtml = '';
|
||||
|
||||
if (isCorrectAnswer) {
|
||||
optionClass += ' correct';
|
||||
badgeClass = 'bg-success';
|
||||
iconHtml = '<i class="fas fa-check ms-2 text-success"></i>';
|
||||
} else if (isUserAnswer && !isCorrect) {
|
||||
optionClass += ' incorrect';
|
||||
badgeClass = 'bg-danger';
|
||||
iconHtml = '<i class="fas fa-times ms-2 text-danger"></i>';
|
||||
} else if (isUserAnswer) {
|
||||
optionClass += ' selected';
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="${optionClass}">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="badge ${badgeClass} me-3">${letter.toUpperCase()}</span>
|
||||
<span>${option.replace(/^[a-d]\)\s*/, '')}</span>
|
||||
</div>
|
||||
${iconHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
// Add explanation if available
|
||||
if (!wasAnswered) {
|
||||
html += `
|
||||
<div class="alert alert-warning mt-3">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<strong>No respondida:</strong> Esta pregunta no fue respondida durante el examen.
|
||||
</div>
|
||||
`;
|
||||
} else if (!isCorrect) {
|
||||
html += `
|
||||
<div class="alert alert-info mt-3">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Tu respuesta:</strong> ${userAnswer?.toUpperCase()} |
|
||||
<strong>Respuesta correcta:</strong> ${question.correct.toUpperCase()}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
html += `
|
||||
<div class="alert alert-success mt-3">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<strong>¡Correcto!</strong> Seleccionaste la respuesta correcta.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
document.getElementById('review-question-container').innerHTML = html;
|
||||
document.getElementById('review-progress').textContent = `${this.reviewIndex + 1} de ${this.questions.length}`;
|
||||
|
||||
this.updateReviewNavigation();
|
||||
this.updateReviewStats();
|
||||
}
|
||||
|
||||
createReviewNavigator() {
|
||||
const navigator = document.getElementById('review-navigator');
|
||||
let html = '';
|
||||
|
||||
this.questions.forEach((question, index) => {
|
||||
const userAnswer = this.examAnswers[question.id];
|
||||
const isCorrect = userAnswer === question.correct;
|
||||
const wasAnswered = userAnswer !== undefined;
|
||||
|
||||
let buttonClass = 'btn btn-sm me-1 mb-1';
|
||||
let iconHtml = '';
|
||||
|
||||
if (wasAnswered) {
|
||||
if (isCorrect) {
|
||||
buttonClass += ' btn-success';
|
||||
iconHtml = '<i class="fas fa-check"></i>';
|
||||
} else {
|
||||
buttonClass += ' btn-danger';
|
||||
iconHtml = '<i class="fas fa-times"></i>';
|
||||
}
|
||||
} else {
|
||||
buttonClass += ' btn-secondary';
|
||||
iconHtml = '<i class="fas fa-question"></i>';
|
||||
}
|
||||
|
||||
if (index === this.reviewIndex) {
|
||||
buttonClass += ' active';
|
||||
}
|
||||
|
||||
html += `
|
||||
<button class="${buttonClass}" data-review-index="${index}">
|
||||
${index + 1} ${iconHtml}
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
|
||||
navigator.innerHTML = html;
|
||||
|
||||
// Add click handlers for navigator buttons
|
||||
navigator.querySelectorAll('button[data-review-index]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
this.reviewIndex = parseInt(button.getAttribute('data-review-index'));
|
||||
this.displayReviewQuestion();
|
||||
this.createReviewNavigator();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateReviewNavigation() {
|
||||
document.getElementById('prev-review-btn').disabled = this.reviewIndex === 0;
|
||||
document.getElementById('next-review-btn').disabled = this.reviewIndex === this.questions.length - 1;
|
||||
}
|
||||
|
||||
updateReviewStats() {
|
||||
let correct = 0, incorrect = 0, unanswered = 0;
|
||||
|
||||
this.questions.forEach(question => {
|
||||
const userAnswer = this.examAnswers[question.id];
|
||||
if (userAnswer === undefined) {
|
||||
unanswered++;
|
||||
} else if (userAnswer === question.correct) {
|
||||
correct++;
|
||||
} else {
|
||||
incorrect++;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('review-correct-count').textContent = correct;
|
||||
document.getElementById('review-incorrect-count').textContent = incorrect;
|
||||
document.getElementById('review-unanswered-count').textContent = unanswered;
|
||||
}
|
||||
|
||||
previousReviewQuestion() {
|
||||
if (this.reviewIndex > 0) {
|
||||
this.reviewIndex--;
|
||||
this.displayReviewQuestion();
|
||||
this.createReviewNavigator();
|
||||
}
|
||||
}
|
||||
|
||||
nextReviewQuestion() {
|
||||
if (this.reviewIndex < this.questions.length - 1) {
|
||||
this.reviewIndex++;
|
||||
this.displayReviewQuestion();
|
||||
this.createReviewNavigator();
|
||||
}
|
||||
}
|
||||
|
||||
backToResults() {
|
||||
BaseModule.hide('exam-review');
|
||||
BaseModule.show('exam-results');
|
||||
}
|
||||
|
||||
handleKeyboard(e) {
|
||||
// Handle keyboard shortcuts during exam
|
||||
if (document.getElementById('exam-content') && document.getElementById('exam-content').style.display !== 'none') {
|
||||
const key = e.key.toLowerCase();
|
||||
|
||||
// Handle option selection (a, b, c, d)
|
||||
if (['a', 'b', 'c', 'd'].includes(key)) {
|
||||
e.preventDefault();
|
||||
const optionButton = document.querySelector(`.option-enhanced[data-answer="${key}"]`);
|
||||
if (optionButton) {
|
||||
// Remove previous selection
|
||||
document.querySelectorAll('.option-enhanced').forEach(btn => btn.classList.remove('selected'));
|
||||
|
||||
// Select the new option
|
||||
optionButton.classList.add('selected');
|
||||
|
||||
// Save the answer
|
||||
const question = this.questions[this.currentIndex];
|
||||
this.examAnswers[question.id] = key;
|
||||
|
||||
if (this.navigator) {
|
||||
this.navigator.updateNavigator();
|
||||
}
|
||||
this.updateExamProgress();
|
||||
this.playSound('click');
|
||||
}
|
||||
}
|
||||
// Navigation keys
|
||||
else if (e.key === 'ArrowLeft' && this.currentIndex > 0) {
|
||||
this.previousQuestion();
|
||||
} else if (e.key === 'ArrowRight' && this.currentIndex < this.questions.length - 1) {
|
||||
this.nextQuestion();
|
||||
}
|
||||
}
|
||||
// Handle keyboard shortcuts during review
|
||||
else if (document.getElementById('exam-review') && document.getElementById('exam-review').style.display !== 'none') {
|
||||
if (e.key === 'ArrowLeft' && this.reviewIndex > 0) {
|
||||
this.previousReviewQuestion();
|
||||
} else if (e.key === 'ArrowRight' && this.reviewIndex < this.questions.length - 1) {
|
||||
this.nextReviewQuestion();
|
||||
} else if (e.key === 'Escape') {
|
||||
this.backToResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (document.getElementById('exam-setup')) {
|
||||
window.examMode = new ExamMode();
|
||||
}
|
||||
});
|
||||
|
||||
// Export for global use
|
||||
window.ExamMode = ExamMode;
|
||||
326
static/js/modules/practice-mode.js
Normal file
@@ -0,0 +1,326 @@
|
||||
// Practice Mode Module
|
||||
class PracticeMode extends BaseModule {
|
||||
constructor() {
|
||||
super();
|
||||
this.sessionStats = {
|
||||
correct: 0,
|
||||
incorrect: 0,
|
||||
total: 0
|
||||
};
|
||||
this.answered = false;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.getElementById('start-practice')?.addEventListener('click', () => {
|
||||
this.startPractice();
|
||||
});
|
||||
|
||||
document.getElementById('new-session')?.addEventListener('click', () => {
|
||||
this.resetSession();
|
||||
});
|
||||
|
||||
document.getElementById('skip-question')?.addEventListener('click', () => {
|
||||
this.nextQuestion();
|
||||
});
|
||||
|
||||
document.getElementById('next-question')?.addEventListener('click', () => {
|
||||
this.nextQuestion();
|
||||
});
|
||||
|
||||
document.getElementById('practice-again')?.addEventListener('click', () => {
|
||||
this.resetSession();
|
||||
this.startPractice();
|
||||
});
|
||||
}
|
||||
|
||||
async startPractice() {
|
||||
const count = parseInt(document.getElementById('question-count').value);
|
||||
|
||||
this.showLoading();
|
||||
BaseModule.hide('welcome-message');
|
||||
BaseModule.hide('practice-content');
|
||||
BaseModule.hide('practice-complete');
|
||||
|
||||
try {
|
||||
const data = await this.fetchQuestions({
|
||||
mode: 'random',
|
||||
count: count
|
||||
});
|
||||
|
||||
this.questions = data;
|
||||
this.currentIndex = 0;
|
||||
this.resetSession();
|
||||
|
||||
this.displayQuestion();
|
||||
|
||||
this.hideLoading();
|
||||
BaseModule.show('practice-content');
|
||||
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
console.error('Error in startPractice:', error);
|
||||
}
|
||||
}
|
||||
|
||||
displayQuestion() {
|
||||
if (this.currentIndex >= this.questions.length) {
|
||||
this.showResults();
|
||||
return;
|
||||
}
|
||||
|
||||
const question = this.questions[this.currentIndex];
|
||||
this.answered = false;
|
||||
|
||||
let html = `
|
||||
<div class="question-card-enhanced fade-in">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0 text-gradient">Pregunta ${question.id}</h5>
|
||||
<span class="badge bg-success">${this.currentIndex + 1} de ${this.questions.length}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (question.has_image && question.image) {
|
||||
html += `<img src="${question.image}" class="question-image" alt="Imagen de la pregunta">`;
|
||||
}
|
||||
|
||||
html += `<p class="lead mb-4">${question.question}</p>`;
|
||||
|
||||
html += '<div class="options-container">';
|
||||
question.options.forEach((option, index) => {
|
||||
const letter = String.fromCharCode(97 + index); // a, b, c, d
|
||||
html += `
|
||||
<button class="option-enhanced" data-answer="${letter}">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="badge bg-primary me-3">${letter.toUpperCase()}</span>
|
||||
<span>${option.replace(/^[a-d]\)\s*/, '')}</span>
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
html += '<div id="feedback" class="mt-3" style="display: none;"></div>';
|
||||
html += '</div>';
|
||||
|
||||
document.getElementById('question-container').innerHTML = html;
|
||||
document.getElementById('question-counter').textContent = `${this.currentIndex + 1} de ${this.questions.length}`;
|
||||
|
||||
// Setup option click handlers
|
||||
this.setupOptionHandlers(question);
|
||||
|
||||
BaseModule.show('skip-question');
|
||||
BaseModule.hide('next-question');
|
||||
|
||||
// Setup keyboard shortcuts
|
||||
this.setupKeyboardHandlers();
|
||||
}
|
||||
|
||||
setupOptionHandlers(question) {
|
||||
const optionButtons = document.querySelectorAll('.option-enhanced');
|
||||
optionButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
if (this.answered) return;
|
||||
|
||||
this.playSound('click');
|
||||
button.classList.add('selected');
|
||||
|
||||
const selectedAnswer = button.getAttribute('data-answer');
|
||||
this.checkAnswer(selectedAnswer, question);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupKeyboardHandlers() {
|
||||
document.removeEventListener('keydown', this.practiceKeyHandler);
|
||||
document.addEventListener('keydown', this.practiceKeyHandler.bind(this));
|
||||
}
|
||||
|
||||
practiceKeyHandler(e) {
|
||||
if (this.answered) return;
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
if (['a', 'b', 'c', 'd'].includes(key)) {
|
||||
e.preventDefault();
|
||||
const optionButton = document.querySelector(`.option-enhanced[data-answer="${key}"]`);
|
||||
if (optionButton) {
|
||||
optionButton.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkAnswer(selectedAnswer, question) {
|
||||
if (this.answered) return;
|
||||
|
||||
this.answered = true;
|
||||
const isCorrect = selectedAnswer === question.correct;
|
||||
|
||||
// Update session stats
|
||||
this.sessionStats.total++;
|
||||
if (isCorrect) {
|
||||
this.sessionStats.correct++;
|
||||
this.playSound('correct');
|
||||
} else {
|
||||
this.sessionStats.incorrect++;
|
||||
this.playSound('incorrect');
|
||||
}
|
||||
|
||||
this.updateSessionStats();
|
||||
|
||||
// Update global stats
|
||||
if (window.statsManager) {
|
||||
window.statsManager.updateStats(isCorrect);
|
||||
}
|
||||
|
||||
// Show feedback
|
||||
this.showFeedback(isCorrect, question);
|
||||
|
||||
BaseModule.hide('skip-question');
|
||||
BaseModule.show('next-question');
|
||||
}
|
||||
|
||||
showFeedback(isCorrect, question) {
|
||||
const optionButtons = document.querySelectorAll('.option-enhanced');
|
||||
optionButtons.forEach(button => {
|
||||
const answer = button.getAttribute('data-answer');
|
||||
if (answer === question.correct) {
|
||||
button.classList.add('correct');
|
||||
} else if (button.classList.contains('selected') && !isCorrect) {
|
||||
button.classList.add('incorrect');
|
||||
}
|
||||
button.style.pointerEvents = 'none';
|
||||
});
|
||||
|
||||
let feedbackHtml = '';
|
||||
if (isCorrect) {
|
||||
feedbackHtml = `
|
||||
<div class="alert alert-success border-0 shadow-sm">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-check-circle fa-2x text-success me-3"></i>
|
||||
<div>
|
||||
<h6 class="mb-1">¡Excelente!</h6>
|
||||
<p class="mb-0">Has seleccionado la respuesta correcta.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
feedbackHtml = `
|
||||
<div class="alert alert-danger border-0 shadow-sm">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-times-circle fa-2x text-danger me-3"></i>
|
||||
<div>
|
||||
<h6 class="mb-1">Respuesta incorrecta</h6>
|
||||
<p class="mb-0">La respuesta correcta es: <strong>${question.correct.toUpperCase()})</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const feedbackElement = document.getElementById('feedback');
|
||||
feedbackElement.innerHTML = feedbackHtml;
|
||||
feedbackElement.style.display = 'none';
|
||||
feedbackElement.style.opacity = '0';
|
||||
feedbackElement.style.display = 'block';
|
||||
|
||||
// Fade in effect
|
||||
setTimeout(() => {
|
||||
feedbackElement.style.transition = 'opacity 0.3s ease';
|
||||
feedbackElement.style.opacity = '1';
|
||||
}, 10);
|
||||
}
|
||||
|
||||
nextQuestion() {
|
||||
document.removeEventListener('keydown', this.practiceKeyHandler);
|
||||
this.currentIndex++;
|
||||
this.displayQuestion();
|
||||
}
|
||||
|
||||
updateSessionStats() {
|
||||
document.getElementById('session-correct').textContent = this.sessionStats.correct;
|
||||
document.getElementById('session-incorrect').textContent = this.sessionStats.incorrect;
|
||||
|
||||
const accuracy = this.sessionStats.total > 0 ?
|
||||
Math.round((this.sessionStats.correct / this.sessionStats.total) * 100) : 0;
|
||||
document.getElementById('session-accuracy').textContent = accuracy + '%';
|
||||
|
||||
// Update progress bar
|
||||
const progressBar = document.getElementById('session-progress');
|
||||
progressBar.style.width = accuracy + '%';
|
||||
|
||||
// Change bar color based on accuracy
|
||||
progressBar.classList.remove('bg-danger', 'bg-warning', 'bg-success', 'bg-light');
|
||||
|
||||
if (accuracy >= 80) {
|
||||
progressBar.classList.add('bg-success');
|
||||
} else if (accuracy >= 60) {
|
||||
progressBar.classList.add('bg-warning');
|
||||
} else if (accuracy > 0) {
|
||||
progressBar.classList.add('bg-danger');
|
||||
} else {
|
||||
progressBar.classList.add('bg-light');
|
||||
}
|
||||
}
|
||||
|
||||
resetSession() {
|
||||
this.sessionStats = {
|
||||
correct: 0,
|
||||
incorrect: 0,
|
||||
total: 0
|
||||
};
|
||||
this.updateSessionStats();
|
||||
}
|
||||
|
||||
showResults() {
|
||||
const accuracy = this.sessionStats.total > 0 ?
|
||||
Math.round((this.sessionStats.correct / this.sessionStats.total) * 100) : 0;
|
||||
|
||||
document.getElementById('final-correct').textContent = this.sessionStats.correct;
|
||||
document.getElementById('final-incorrect').textContent = this.sessionStats.incorrect;
|
||||
document.getElementById('final-accuracy').textContent = accuracy + '%';
|
||||
|
||||
const finalProgress = document.getElementById('final-progress');
|
||||
finalProgress.style.width = accuracy + '%';
|
||||
|
||||
// Change final progress bar color
|
||||
finalProgress.classList.remove('bg-danger', 'bg-warning', 'bg-success');
|
||||
|
||||
if (accuracy >= 80) {
|
||||
finalProgress.classList.add('bg-success');
|
||||
if (window.Utils) {
|
||||
Utils.createConfetti();
|
||||
Utils.playSound('complete');
|
||||
Utils.showNotification('¡Excelente trabajo! Has obtenido una puntuación sobresaliente.', 'success');
|
||||
}
|
||||
} else if (accuracy >= 60) {
|
||||
finalProgress.classList.add('bg-warning');
|
||||
if (window.Utils) {
|
||||
Utils.playSound('complete');
|
||||
Utils.showNotification('¡Buen trabajo! Sigue practicando para mejorar.', 'warning');
|
||||
}
|
||||
} else {
|
||||
finalProgress.classList.add('bg-danger');
|
||||
if (window.Utils) {
|
||||
Utils.showNotification('Sigue estudiando. La práctica hace al maestro.', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
BaseModule.hide('practice-content');
|
||||
BaseModule.show('practice-complete');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (document.getElementById('practice-content')) {
|
||||
window.practiceMode = new PracticeMode();
|
||||
}
|
||||
});
|
||||
|
||||
// Export for global use
|
||||
window.PracticeMode = PracticeMode;
|
||||
223
static/js/modules/question-navigator.js
Normal file
@@ -0,0 +1,223 @@
|
||||
// Question Navigator Module
|
||||
class QuestionNavigator {
|
||||
constructor(containerId, options = {}) {
|
||||
this.containerId = containerId;
|
||||
this.questions = [];
|
||||
this.currentIndex = 0;
|
||||
this.visitedQuestions = new Set();
|
||||
this.onQuestionSelect = options.onQuestionSelect || (() => {});
|
||||
this.storagePrefix = options.storagePrefix || 'navigator';
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Jump button
|
||||
const jumpBtn = document.getElementById('jump-btn');
|
||||
if (jumpBtn) {
|
||||
jumpBtn.addEventListener('click', () => this.jumpToQuestion());
|
||||
}
|
||||
|
||||
// Jump input
|
||||
const jumpInput = document.getElementById('jump-to-question');
|
||||
if (jumpInput) {
|
||||
jumpInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.jumpToQuestion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reset button
|
||||
const resetBtn = document.getElementById('reset-visited-btn');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => this.resetVisitedQuestions());
|
||||
}
|
||||
}
|
||||
|
||||
setQuestions(questions) {
|
||||
this.questions = questions;
|
||||
this.createNavigator();
|
||||
this.loadVisitedFromStorage();
|
||||
this.updateNavigator();
|
||||
}
|
||||
|
||||
createNavigator() {
|
||||
const container = document.getElementById(this.containerId);
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
this.questions.forEach((question, index) => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'question-nav-btn';
|
||||
button.textContent = question.id;
|
||||
button.setAttribute('data-index', index);
|
||||
button.title = `Pregunta ${question.id}`;
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
this.selectQuestion(index);
|
||||
});
|
||||
|
||||
container.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
selectQuestion(index) {
|
||||
if (index >= 0 && index < this.questions.length) {
|
||||
this.currentIndex = index;
|
||||
this.visitedQuestions.add(index);
|
||||
this.saveVisitedToStorage();
|
||||
this.updateNavigator();
|
||||
this.onQuestionSelect(index);
|
||||
}
|
||||
}
|
||||
|
||||
updateNavigator() {
|
||||
const buttons = document.querySelectorAll(`#${this.containerId} .question-nav-btn`);
|
||||
|
||||
buttons.forEach((button, index) => {
|
||||
button.classList.remove('current', 'visited');
|
||||
|
||||
if (index === this.currentIndex) {
|
||||
button.classList.add('current');
|
||||
this.visitedQuestions.add(index);
|
||||
this.saveVisitedToStorage();
|
||||
// Scroll to current button
|
||||
button.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'center'
|
||||
});
|
||||
} else if (this.visitedQuestions.has(index)) {
|
||||
button.classList.add('visited');
|
||||
}
|
||||
});
|
||||
|
||||
this.updateProgress();
|
||||
this.updateJumpInputMax();
|
||||
}
|
||||
|
||||
updateProgress() {
|
||||
const progressEl = document.getElementById('navigator-progress');
|
||||
if (progressEl) {
|
||||
progressEl.textContent = `${this.visitedQuestions.size} de ${this.questions.length} visitadas`;
|
||||
}
|
||||
}
|
||||
|
||||
updateJumpInputMax() {
|
||||
const jumpInput = document.getElementById('jump-to-question');
|
||||
if (jumpInput && this.questions.length > 0) {
|
||||
jumpInput.max = this.questions[this.questions.length - 1].id;
|
||||
}
|
||||
}
|
||||
|
||||
jumpToQuestion() {
|
||||
const jumpInput = document.getElementById('jump-to-question');
|
||||
if (!jumpInput) return;
|
||||
|
||||
const questionId = parseInt(jumpInput.value);
|
||||
|
||||
if (!questionId || questionId < 1) {
|
||||
this.showNotification('Por favor ingresa un número de pregunta válido', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const questionIndex = this.questions.findIndex(q => q.id === questionId);
|
||||
|
||||
if (questionIndex === -1) {
|
||||
this.showNotification(`La pregunta ${questionId} no está en el rango actual`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectQuestion(questionIndex);
|
||||
jumpInput.value = '';
|
||||
|
||||
this.playSound('click');
|
||||
this.showNotification(`Navegando a pregunta ${questionId}`, 'success', 2000);
|
||||
}
|
||||
|
||||
resetVisitedQuestions() {
|
||||
if (this.visitedQuestions.size === 0) {
|
||||
this.showNotification('No hay preguntas visitadas para limpiar', 'info', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmReset = confirm(
|
||||
`¿Estás seguro de que quieres limpiar el progreso de ${this.visitedQuestions.size} preguntas visitadas?`
|
||||
);
|
||||
|
||||
if (confirmReset) {
|
||||
// Clear visited questions but keep current question
|
||||
this.visitedQuestions.clear();
|
||||
this.visitedQuestions.add(this.currentIndex);
|
||||
|
||||
this.saveVisitedToStorage();
|
||||
this.updateNavigator();
|
||||
|
||||
this.playSound('complete');
|
||||
this.showNotification('Progreso de preguntas visitadas limpiado', 'success', 3000);
|
||||
|
||||
// Animation for reset button
|
||||
const resetBtn = document.getElementById('reset-visited-btn');
|
||||
if (resetBtn) {
|
||||
resetBtn.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => {
|
||||
resetBtn.style.transform = 'scale(1)';
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveVisitedToStorage() {
|
||||
try {
|
||||
const storageKey = `${this.storagePrefix}_visited`;
|
||||
localStorage.setItem(storageKey, JSON.stringify(Array.from(this.visitedQuestions)));
|
||||
} catch (error) {
|
||||
console.warn('Could not save visited questions to localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadVisitedFromStorage() {
|
||||
try {
|
||||
const storageKey = `${this.storagePrefix}_visited`;
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved) {
|
||||
const visitedArray = JSON.parse(saved);
|
||||
this.visitedQuestions = new Set(visitedArray);
|
||||
} else {
|
||||
this.visitedQuestions = new Set();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not load visited questions from localStorage:', error);
|
||||
this.visitedQuestions = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentIndex(index) {
|
||||
this.currentIndex = index;
|
||||
this.updateNavigator();
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
showNotification(message, type = 'info', duration = 3000) {
|
||||
if (window.Utils) {
|
||||
Utils.showNotification(message, type, duration);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
playSound(type) {
|
||||
if (window.Utils) {
|
||||
Utils.playSound(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for global use
|
||||
window.QuestionNavigator = QuestionNavigator;
|
||||
222
static/js/modules/stats-display.js
Normal file
@@ -0,0 +1,222 @@
|
||||
// Stats Display Module for Index Page
|
||||
class StatsDisplay {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.loadStats();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Reset stats modal listeners
|
||||
document.getElementById('reset-stats-btn')?.addEventListener('click', () => {
|
||||
this.showResetStatsModal();
|
||||
});
|
||||
|
||||
document.getElementById('resetStatsModalClose')?.addEventListener('click', () => {
|
||||
this.hideResetStatsModal();
|
||||
});
|
||||
|
||||
document.getElementById('resetStatsModalCancel')?.addEventListener('click', () => {
|
||||
this.hideResetStatsModal();
|
||||
});
|
||||
|
||||
document.getElementById('resetStatsBackdrop')?.addEventListener('click', () => {
|
||||
this.hideResetStatsModal();
|
||||
});
|
||||
|
||||
document.getElementById('confirmResetStats')?.addEventListener('click', () => {
|
||||
this.resetStats();
|
||||
});
|
||||
|
||||
// Keyboard support for modal
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const modal = document.getElementById('resetStatsModal');
|
||||
if (modal && modal.style.display === 'block') {
|
||||
if (e.key === 'Escape') {
|
||||
this.hideResetStatsModal();
|
||||
} else if (e.key === 'Enter') {
|
||||
const focusedElement = document.activeElement;
|
||||
if (focusedElement.id === 'confirmResetStats') {
|
||||
this.resetStats();
|
||||
} else {
|
||||
this.hideResetStatsModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadStats() {
|
||||
// Load statistics from localStorage
|
||||
let stats = {
|
||||
total_answered: 0,
|
||||
correct_answers: 0,
|
||||
incorrect_answers: 0,
|
||||
accuracy: 0
|
||||
};
|
||||
|
||||
// Try to load from localStorage
|
||||
try {
|
||||
const savedStats = localStorage.getItem('balotario_stats');
|
||||
if (savedStats) {
|
||||
const parsedStats = JSON.parse(savedStats);
|
||||
stats = {
|
||||
total_answered: parsedStats.totalAnswered || 0,
|
||||
correct_answers: parsedStats.correctAnswers || 0,
|
||||
incorrect_answers: parsedStats.incorrectAnswers || 0,
|
||||
accuracy: parsedStats.accuracy || 0
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error loading stats from localStorage:', error);
|
||||
}
|
||||
|
||||
// Update UI
|
||||
this.updateStatsDisplay(stats);
|
||||
}
|
||||
|
||||
updateStatsDisplay(stats) {
|
||||
const totalAnsweredEl = document.getElementById('total-answered');
|
||||
const accuracyEl = document.getElementById('accuracy');
|
||||
const accuracyBarEl = document.getElementById('accuracy-bar');
|
||||
const resetBtnEl = document.getElementById('reset-stats-btn');
|
||||
|
||||
if (totalAnsweredEl) totalAnsweredEl.textContent = stats.total_answered;
|
||||
if (accuracyEl) accuracyEl.textContent = stats.accuracy + '%';
|
||||
if (accuracyBarEl) accuracyBarEl.style.width = stats.accuracy + '%';
|
||||
|
||||
// Show/hide reset button based on stats
|
||||
if (resetBtnEl) {
|
||||
if (stats.total_answered > 0) {
|
||||
resetBtnEl.style.display = 'inline-block';
|
||||
} else {
|
||||
resetBtnEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showResetStatsModal() {
|
||||
if (window.Utils) {
|
||||
Utils.playSound('click');
|
||||
}
|
||||
|
||||
const modal = document.getElementById('resetStatsModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'block';
|
||||
document.body.classList.add('modal-open');
|
||||
|
||||
// Focus cancel button for accessibility
|
||||
setTimeout(() => {
|
||||
const cancelBtn = document.getElementById('resetStatsModalCancel');
|
||||
if (cancelBtn) cancelBtn.focus();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
hideResetStatsModal() {
|
||||
const dialog = document.getElementById('resetStatsDialog');
|
||||
if (dialog) {
|
||||
dialog.classList.add('modal-closing');
|
||||
|
||||
setTimeout(() => {
|
||||
const modal = document.getElementById('resetStatsModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
dialog.classList.remove('modal-closing');
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
resetStats() {
|
||||
// Show loading in button
|
||||
const confirmBtn = document.getElementById('confirmResetStats');
|
||||
if (!confirmBtn) return;
|
||||
|
||||
const originalText = confirmBtn.innerHTML;
|
||||
confirmBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Restableciendo...';
|
||||
confirmBtn.disabled = true;
|
||||
|
||||
// Reset statistics directly in localStorage
|
||||
try {
|
||||
// Reset using statsManager if available
|
||||
if (window.statsManager) {
|
||||
window.statsManager.resetStats();
|
||||
} else {
|
||||
// Reset directly in localStorage
|
||||
const resetStats = {
|
||||
totalAnswered: 0,
|
||||
correctAnswers: 0,
|
||||
incorrectAnswers: 0,
|
||||
accuracy: 0,
|
||||
studyTime: 0,
|
||||
examsTaken: 0,
|
||||
bestScore: 0,
|
||||
streak: 0,
|
||||
lastActivity: null
|
||||
};
|
||||
localStorage.setItem('balotario_stats', JSON.stringify(resetStats));
|
||||
}
|
||||
|
||||
// Simulate a small pause for better UX
|
||||
setTimeout(() => {
|
||||
// Reload statistics
|
||||
this.loadStats();
|
||||
|
||||
// Hide modal
|
||||
this.hideResetStatsModal();
|
||||
|
||||
// Restore button
|
||||
confirmBtn.innerHTML = originalText;
|
||||
confirmBtn.disabled = false;
|
||||
|
||||
// Show success notification
|
||||
if (window.Utils) {
|
||||
Utils.showNotification('Estadísticas restablecidas correctamente', 'success', 4000);
|
||||
Utils.playSound('complete');
|
||||
} else {
|
||||
alert('Estadísticas restablecidas correctamente');
|
||||
}
|
||||
|
||||
// Small animation on stats card
|
||||
const statsCard = document.querySelector('.stats-card');
|
||||
if (statsCard) {
|
||||
statsCard.style.transform = 'scale(1.05)';
|
||||
statsCard.style.transition = 'transform 0.3s ease';
|
||||
|
||||
setTimeout(() => {
|
||||
statsCard.style.transform = 'scale(1)';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
}, 800);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error resetting stats:', error);
|
||||
|
||||
// Restore button on error
|
||||
confirmBtn.innerHTML = originalText;
|
||||
confirmBtn.disabled = false;
|
||||
|
||||
if (window.Utils) {
|
||||
Utils.showNotification('Error al restablecer estadísticas', 'danger');
|
||||
} else {
|
||||
alert('Error al restablecer estadísticas');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (document.getElementById('stats-content')) {
|
||||
window.statsDisplay = new StatsDisplay();
|
||||
}
|
||||
});
|
||||
|
||||
// Export for global use
|
||||
window.StatsDisplay = StatsDisplay;
|
||||
229
static/js/modules/study-mode.js
Normal file
@@ -0,0 +1,229 @@
|
||||
// Study Mode Module
|
||||
class StudyMode extends BaseModule {
|
||||
constructor() {
|
||||
super();
|
||||
this.showingAnswer = false;
|
||||
this.navigator = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.loadQuestions();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Filter controls
|
||||
document.getElementById('apply-filters')?.addEventListener('click', () => {
|
||||
this.loadQuestions();
|
||||
});
|
||||
|
||||
document.getElementById('reset-filters')?.addEventListener('click', () => {
|
||||
document.getElementById('start-question').value = 1;
|
||||
document.getElementById('end-question').value = 200;
|
||||
this.loadQuestions();
|
||||
});
|
||||
|
||||
// Navigation controls
|
||||
document.getElementById('prev-btn')?.addEventListener('click', () => {
|
||||
this.previousQuestion();
|
||||
});
|
||||
|
||||
document.getElementById('next-btn')?.addEventListener('click', () => {
|
||||
this.nextQuestion();
|
||||
});
|
||||
|
||||
// Answer controls
|
||||
document.getElementById('show-answer')?.addEventListener('click', () => {
|
||||
this.showAnswer();
|
||||
});
|
||||
|
||||
document.getElementById('hide-answer')?.addEventListener('click', () => {
|
||||
this.hideAnswer();
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
this.handleKeyboard(e);
|
||||
});
|
||||
}
|
||||
|
||||
async loadQuestions() {
|
||||
this.showLoading();
|
||||
BaseModule.hide('study-content');
|
||||
BaseModule.hide('no-questions');
|
||||
|
||||
const start = document.getElementById('start-question').value;
|
||||
const end = document.getElementById('end-question').value;
|
||||
|
||||
try {
|
||||
const data = await this.fetchQuestions({
|
||||
mode: 'range',
|
||||
start: start,
|
||||
end: end
|
||||
});
|
||||
|
||||
this.questions = data;
|
||||
this.currentIndex = 0;
|
||||
|
||||
if (this.questions.length === 0) {
|
||||
this.hideLoading();
|
||||
BaseModule.show('no-questions');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('total-questions').textContent = this.questions.length;
|
||||
|
||||
// Initialize navigator
|
||||
this.initializeNavigator(start, end);
|
||||
|
||||
this.displayQuestion();
|
||||
this.updateProgress('study-progress');
|
||||
|
||||
this.hideLoading();
|
||||
BaseModule.show('study-content');
|
||||
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
console.error('Error in loadQuestions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
initializeNavigator(start, end) {
|
||||
if (!this.navigator) {
|
||||
this.navigator = new QuestionNavigator('question-navigator', {
|
||||
onQuestionSelect: (index) => {
|
||||
this.currentIndex = index;
|
||||
this.displayQuestion();
|
||||
this.hideAnswer();
|
||||
},
|
||||
storagePrefix: `study_${start}_${end}`
|
||||
});
|
||||
}
|
||||
|
||||
this.navigator.setQuestions(this.questions);
|
||||
}
|
||||
|
||||
displayQuestion() {
|
||||
if (this.questions.length === 0) return;
|
||||
|
||||
const question = this.questions[this.currentIndex];
|
||||
|
||||
let html = this.createQuestionHTML(question, this.currentIndex, this.questions.length);
|
||||
html += this.createOptionsHTML(question.options, question.correct);
|
||||
|
||||
document.getElementById('question-container').innerHTML = html;
|
||||
|
||||
// Animate options
|
||||
this.animateOptions();
|
||||
|
||||
this.updateNavigation('prev-btn', 'next-btn', 'current-question');
|
||||
this.updateProgress('study-progress');
|
||||
|
||||
if (this.navigator) {
|
||||
this.navigator.setCurrentIndex(this.currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
animateOptions() {
|
||||
const options = document.querySelectorAll('.option-btn');
|
||||
options.forEach((option, index) => {
|
||||
option.style.opacity = '0';
|
||||
option.style.transform = 'translateY(20px)';
|
||||
setTimeout(() => {
|
||||
option.style.transition = 'all 0.3s ease';
|
||||
option.style.opacity = '1';
|
||||
option.style.transform = 'translateY(0)';
|
||||
}, index * 100);
|
||||
});
|
||||
}
|
||||
|
||||
showAnswer() {
|
||||
const correctOption = document.querySelector('.option-btn[data-correct="true"]');
|
||||
if (correctOption) {
|
||||
correctOption.classList.add('correct', 'correct-option');
|
||||
correctOption.style.background = 'rgba(39, 174, 96, 0.3)';
|
||||
correctOption.style.borderColor = '#27ae60';
|
||||
correctOption.style.color = '#ffffff';
|
||||
correctOption.style.fontWeight = 'bold';
|
||||
|
||||
const indicator = correctOption.querySelector('.answer-indicator');
|
||||
if (indicator) {
|
||||
indicator.style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
// Dim incorrect options
|
||||
const incorrectOptions = document.querySelectorAll('.option-btn[data-correct="false"]');
|
||||
incorrectOptions.forEach(option => {
|
||||
option.classList.add('text-muted');
|
||||
option.style.opacity = '0.6';
|
||||
});
|
||||
|
||||
BaseModule.hide('show-answer');
|
||||
BaseModule.show('hide-answer');
|
||||
this.showingAnswer = true;
|
||||
}
|
||||
|
||||
hideAnswer() {
|
||||
const allOptions = document.querySelectorAll('.option-btn');
|
||||
allOptions.forEach(option => {
|
||||
option.classList.remove('correct', 'correct-option', 'text-muted');
|
||||
option.style.background = '';
|
||||
option.style.borderColor = '';
|
||||
option.style.color = '';
|
||||
option.style.fontWeight = '';
|
||||
option.style.opacity = '';
|
||||
});
|
||||
|
||||
const indicators = document.querySelectorAll('.answer-indicator');
|
||||
indicators.forEach(indicator => {
|
||||
indicator.style.display = 'none';
|
||||
});
|
||||
|
||||
BaseModule.show('show-answer');
|
||||
BaseModule.hide('hide-answer');
|
||||
this.showingAnswer = false;
|
||||
}
|
||||
|
||||
previousQuestion() {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
this.displayQuestion();
|
||||
this.hideAnswer();
|
||||
}
|
||||
}
|
||||
|
||||
nextQuestion() {
|
||||
if (this.currentIndex < this.questions.length - 1) {
|
||||
this.currentIndex++;
|
||||
this.displayQuestion();
|
||||
this.hideAnswer();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyboard(e) {
|
||||
if (e.key === 'ArrowLeft' && this.currentIndex > 0) {
|
||||
this.previousQuestion();
|
||||
} else if (e.key === 'ArrowRight' && this.currentIndex < this.questions.length - 1) {
|
||||
this.nextQuestion();
|
||||
} else if (e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (this.showingAnswer) {
|
||||
this.hideAnswer();
|
||||
} else {
|
||||
this.showAnswer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (document.getElementById('study-content')) {
|
||||
window.studyMode = new StudyMode();
|
||||
}
|
||||
});
|
||||
|
||||
// Export for global use
|
||||
window.StudyMode = StudyMode;
|
||||
28
static/site.webmanifest
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Balotario Licencia A-I",
|
||||
"short_name": "Balotario A-I",
|
||||
"description": "Aplicación web para estudiar el balotario de licencia de conducir Clase A - Categoría I",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon.svg",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
],
|
||||
"theme_color": "#3498db",
|
||||
"background_color": "#2c3e50",
|
||||
"display": "standalone",
|
||||
"start_url": "/",
|
||||
"scope": "/"
|
||||
}
|
||||