- Remove original client/, config/, fonts/, src/, templates/ directories - Clean up miscellaneous test HTML files - Maintain clean project structure after reorganization - All functionality preserved in packages/core 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
6804 lines
276 KiB
HTML
6804 lines
276 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Soroban Flashcards</title>
|
||
<style>
|
||
body {
|
||
font-family: DejaVu Sans, sans-serif;
|
||
margin: 0;
|
||
padding: 20px;
|
||
background-color: #f5f5f5;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
/* Navigation Styles */
|
||
.page-nav {
|
||
position: sticky;
|
||
top: 0;
|
||
background: #fff;
|
||
border-bottom: 2px solid #e9ecef;
|
||
z-index: 1000;
|
||
margin: -20px -20px 30px -20px;
|
||
padding: 15px 20px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.nav-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
}
|
||
|
||
.nav-btn {
|
||
padding: 10px 20px;
|
||
border: 2px solid #2c5f76;
|
||
background: white;
|
||
color: #2c5f76;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.2s ease;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.nav-btn:hover {
|
||
background: #f8f9fa;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.nav-btn.active {
|
||
background: #2c5f76;
|
||
color: white;
|
||
}
|
||
|
||
/* Section Styles */
|
||
.page-section {
|
||
display: none;
|
||
animation: fadeIn 0.3s ease-in-out;
|
||
}
|
||
|
||
.page-section.active {
|
||
display: block;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.section-header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 2rem;
|
||
color: #2c5f76;
|
||
margin: 0 0 10px 0;
|
||
}
|
||
|
||
.section-subtitle {
|
||
color: #666;
|
||
font-size: 1.1rem;
|
||
margin: 0;
|
||
}
|
||
|
||
.header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.header h1 {
|
||
color: #333;
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.header p {
|
||
color: #666;
|
||
font-size: 1.2em;
|
||
}
|
||
|
||
.cards-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||
gap: 20px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.flashcard {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
cursor: pointer;
|
||
position: relative;
|
||
min-height: 220px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.flashcard:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.abacus-container {
|
||
flex: 1;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin: 10px 0;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.abacus-container svg {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
.numeral {
|
||
font-size: 48pt;
|
||
font-weight: bold;
|
||
color: #333;
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: rgba(255,255,255,0.95);
|
||
padding: 15px 25px;
|
||
border-radius: 8px;
|
||
border: 2px solid #ddd;
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||
z-index: 10;
|
||
}
|
||
|
||
.flashcard:hover .numeral {
|
||
opacity: 1;
|
||
}
|
||
|
||
.card-number {
|
||
position: absolute;
|
||
top: 10px;
|
||
left: 10px;
|
||
font-size: 0.8em;
|
||
color: #999;
|
||
background: rgba(255,255,255,0.8);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.instructions {
|
||
text-align: center;
|
||
margin: 30px 0;
|
||
padding: 20px;
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.instructions h3 {
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.instructions p {
|
||
color: #666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.stats {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.stats div {
|
||
background: #f8f9fa;
|
||
padding: 10px 15px;
|
||
border-radius: 6px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.cards-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 15px;
|
||
}
|
||
|
||
.flashcard {
|
||
min-height: 120px;
|
||
padding: 15px;
|
||
}
|
||
|
||
.numeral {
|
||
font-size: calc(48pt * 0.8);
|
||
padding: 10px 20px;
|
||
}
|
||
}
|
||
|
||
/* Quiz Styling */
|
||
.quiz-section {
|
||
background: #f8f9fa;
|
||
border-radius: 12px;
|
||
padding: 30px;
|
||
margin: 30px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.quiz-section h2 {
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.quiz-controls {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.control-group {
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.control-group label {
|
||
display: block;
|
||
font-weight: bold;
|
||
margin-bottom: 10px;
|
||
color: #555;
|
||
}
|
||
|
||
.count-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.count-btn {
|
||
background: white;
|
||
border: 2px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 10px 20px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.count-btn:hover {
|
||
border-color: #4a90e2;
|
||
background: #f0f7ff;
|
||
}
|
||
|
||
.count-btn.active {
|
||
background: #4a90e2;
|
||
color: white;
|
||
border-color: #4a90e2;
|
||
}
|
||
|
||
.slider-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 15px;
|
||
}
|
||
|
||
.slider-container input[type="range"] {
|
||
width: 200px;
|
||
height: 6px;
|
||
border-radius: 3px;
|
||
background: #ddd;
|
||
outline: none;
|
||
-webkit-appearance: none;
|
||
}
|
||
|
||
.slider-container input[type="range"]::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: #4a90e2;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.slider-value {
|
||
font-weight: bold;
|
||
color: #4a90e2;
|
||
min-width: 50px;
|
||
}
|
||
|
||
.quiz-start-btn {
|
||
background: #28a745;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 15px 30px;
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: background 0.2s ease;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.quiz-start-btn:hover {
|
||
background: #218838;
|
||
}
|
||
|
||
/* Quiz Game Area */
|
||
.quiz-game {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
}
|
||
|
||
.quiz-progress {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.progress-bar {
|
||
width: 100%;
|
||
max-width: 400px;
|
||
height: 8px;
|
||
background: #ddd;
|
||
border-radius: 4px;
|
||
margin: 0 auto 10px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: #4a90e2;
|
||
transition: width 0.3s ease;
|
||
width: 0%;
|
||
}
|
||
|
||
.progress-text {
|
||
color: #666;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.quiz-display {
|
||
position: relative;
|
||
min-height: 300px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.quiz-flashcard {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||
width: min(85vw, 700px);
|
||
height: min(50vh, 400px);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0 auto;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
/* Ensure quiz game section fits in viewport */
|
||
#quiz-game {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 20px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* Responsive adjustments for smaller screens */
|
||
@media (max-height: 600px) {
|
||
.quiz-flashcard {
|
||
height: min(40vh, 300px);
|
||
padding: 15px;
|
||
}
|
||
}
|
||
|
||
@media (max-height: 500px) {
|
||
.quiz-flashcard {
|
||
height: min(35vh, 250px);
|
||
padding: 10px;
|
||
}
|
||
|
||
#quiz-game {
|
||
min-height: auto;
|
||
padding: 10px;
|
||
}
|
||
}
|
||
|
||
.quiz-flashcard svg {
|
||
width: 100%;
|
||
height: 100%;
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
}
|
||
|
||
.quiz-flashcard.pulse {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.quiz-flashcard.card-exit-warning {
|
||
border-color: #dc3545;
|
||
box-shadow: 0 0 15px rgba(220, 53, 69, 0.4);
|
||
}
|
||
|
||
.quiz-flashcard.card-fade-out {
|
||
opacity: 0.3;
|
||
transform: scale(0.95);
|
||
transition: all 0.1s ease;
|
||
}
|
||
|
||
.countdown {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
font-size: 48px;
|
||
font-weight: bold;
|
||
color: #4a90e2;
|
||
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
||
z-index: 10;
|
||
}
|
||
|
||
.countdown.ready {
|
||
color: #28a745;
|
||
}
|
||
|
||
.countdown.go {
|
||
color: #ffc107;
|
||
}
|
||
|
||
.countdown.new-card-flash {
|
||
color: #17a2b8;
|
||
font-size: 24px;
|
||
animation: flashIn 0.15s ease;
|
||
}
|
||
|
||
@keyframes flashIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translate(-50%, -50%) scale(0.8);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translate(-50%, -50%) scale(1);
|
||
}
|
||
}
|
||
|
||
/* Quiz Input */
|
||
.quiz-input {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
max-width: 700px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.quiz-stats {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 30px;
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.stats-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.stats-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.stats-item span:last-child {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #2c5f76;
|
||
}
|
||
|
||
.smart-input-container {
|
||
position: relative;
|
||
margin: 40px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.smart-input-prompt {
|
||
font-size: 16px;
|
||
color: #7a8695;
|
||
margin-bottom: 15px;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.number-display {
|
||
min-height: 60px;
|
||
padding: 20px;
|
||
font-size: 32px;
|
||
font-family: 'Courier New', 'Monaco', monospace;
|
||
text-align: center;
|
||
font-weight: bold;
|
||
color: #2c3e50;
|
||
letter-spacing: 3px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.current-typing {
|
||
display: inline-block;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.number-display.correct .current-typing {
|
||
color: #28a745;
|
||
animation: successPulse 0.5s ease;
|
||
}
|
||
|
||
.number-display.incorrect .current-typing {
|
||
color: #dc3545;
|
||
animation: errorShake 0.5s ease;
|
||
}
|
||
|
||
@keyframes successPulse {
|
||
0%, 100% { transform: scale(1); }
|
||
50% { transform: scale(1.05); }
|
||
}
|
||
|
||
@keyframes errorShake {
|
||
0%, 100% { transform: translateX(0); }
|
||
25% { transform: translateX(-10px); }
|
||
75% { transform: translateX(10px); }
|
||
}
|
||
|
||
.input-feedback {
|
||
margin-top: 10px;
|
||
font-weight: bold;
|
||
min-height: 20px;
|
||
}
|
||
|
||
.input-feedback.success {
|
||
color: #28a745;
|
||
}
|
||
|
||
.input-feedback.error {
|
||
color: #dc3545;
|
||
}
|
||
|
||
.found-numbers {
|
||
margin: 30px 0;
|
||
min-height: 60px;
|
||
}
|
||
|
||
.found-number {
|
||
display: inline-block;
|
||
margin: 5px;
|
||
padding: 10px 15px;
|
||
background: #28a745;
|
||
color: white;
|
||
border-radius: 8px;
|
||
font-weight: bold;
|
||
font-size: 18px;
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.finish-btn {
|
||
background: #2c5f76;
|
||
color: white;
|
||
border: none;
|
||
padding: 15px 30px;
|
||
font-size: 18px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.finish-btn:hover {
|
||
background: #1e4a5c;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.quiz-finish-buttons {
|
||
display: flex;
|
||
gap: 15px;
|
||
justify-content: center;
|
||
margin-top: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.give-up-btn {
|
||
background: #17a2b8;
|
||
color: white;
|
||
border: none;
|
||
padding: 15px 30px;
|
||
font-size: 18px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.give-up-btn:hover {
|
||
background: #138496;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.input-container {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.quiz-input textarea {
|
||
width: 100%;
|
||
max-width: 400px;
|
||
height: 120px;
|
||
border: 2px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
font-size: 16px;
|
||
resize: vertical;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.quiz-input textarea:focus {
|
||
outline: none;
|
||
border-color: #4a90e2;
|
||
}
|
||
|
||
.quiz-input button {
|
||
background: #4a90e2;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 12px 24px;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.quiz-input button:hover {
|
||
background: #357abd;
|
||
}
|
||
|
||
/* Quiz Results */
|
||
.quiz-results {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
max-width: 700px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.score-display {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 40px;
|
||
margin: 30px 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.score-circle {
|
||
width: 120px;
|
||
height: 120px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #4a90e2, #357abd);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.score-details {
|
||
text-align: left;
|
||
}
|
||
|
||
.score-details p {
|
||
margin: 8px 0;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.results-breakdown {
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin: 30px 0;
|
||
text-align: left;
|
||
}
|
||
|
||
.result-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.result-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.result-correct {
|
||
color: #28a745;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.result-incorrect {
|
||
color: #dc3545;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.quiz-actions {
|
||
margin-top: 30px;
|
||
display: flex;
|
||
gap: 15px;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.quiz-actions button {
|
||
padding: 12px 24px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
transition: background 0.2s ease;
|
||
}
|
||
|
||
#retry-quiz {
|
||
background: #ffc107;
|
||
color: #333;
|
||
}
|
||
|
||
#retry-quiz:hover {
|
||
background: #e0a800;
|
||
}
|
||
|
||
#back-to-cards {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
#back-to-cards:hover {
|
||
background: #545b62;
|
||
}
|
||
|
||
/* End Game Button */
|
||
.end-game-btn {
|
||
background: #dc3545;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: background 0.2s ease;
|
||
}
|
||
|
||
.end-game-btn:hover {
|
||
background: #c82333;
|
||
}
|
||
|
||
/* Quiz Header for Game Mode */
|
||
.quiz-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding: 15px 20px;
|
||
background: rgba(74, 144, 226, 0.1);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.quiz-header .quiz-progress {
|
||
flex: 1;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.quiz-section {
|
||
padding: 20px;
|
||
}
|
||
|
||
.count-buttons {
|
||
gap: 8px;
|
||
}
|
||
|
||
.count-btn {
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.score-display {
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.score-details {
|
||
text-align: center;
|
||
}
|
||
|
||
.countdown {
|
||
font-size: 36px;
|
||
}
|
||
|
||
.sorting-section {
|
||
padding: 20px;
|
||
}
|
||
|
||
.sorting-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
gap: 10px;
|
||
padding: 15px;
|
||
}
|
||
|
||
.sort-card {
|
||
padding: 10px;
|
||
}
|
||
|
||
.sort-count-btn {
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
margin: 2px;
|
||
}
|
||
|
||
.sort-start-btn, .sort-check-btn, .sort-reveal-btn, .sort-new-btn {
|
||
padding: 10px 16px;
|
||
font-size: 14px;
|
||
margin: 5px;
|
||
}
|
||
|
||
.sorting-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
}
|
||
|
||
.challenges-grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 20px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.challenge-category {
|
||
padding: 20px;
|
||
}
|
||
|
||
.challenge-card {
|
||
min-height: 80px;
|
||
padding: 15px;
|
||
}
|
||
|
||
.challenge-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.modal-content {
|
||
width: 95%;
|
||
max-height: 95vh;
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 15px 20px;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
}
|
||
|
||
.modal-header h2 {
|
||
font-size: 18px;
|
||
}
|
||
}
|
||
|
||
/* Card Sorting Styling */
|
||
.sorting-section {
|
||
background: #e8f4f8;
|
||
border-radius: 12px;
|
||
padding: 30px;
|
||
margin: 30px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.sorting-section h2 {
|
||
color: #2c5f76;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.sorting-controls {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.sort-count-btn {
|
||
background: #ffffff;
|
||
color: #2c5f76;
|
||
border: 2px solid #2c5f76;
|
||
border-radius: 8px;
|
||
padding: 10px 20px;
|
||
margin: 0 5px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.sort-count-btn:hover {
|
||
background: #2c5f76;
|
||
color: white;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.sort-count-btn.active {
|
||
background: #2c5f76;
|
||
color: white;
|
||
}
|
||
|
||
.sorting-actions {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* Setup modal body for sticky positioning */
|
||
.modal-body {
|
||
position: relative;
|
||
}
|
||
|
||
/* Sticky header within modal body */
|
||
.sorting-header {
|
||
position: sticky;
|
||
top: 0;
|
||
background: #f8f9fa;
|
||
border-bottom: 2px solid #e1e5e9;
|
||
border-radius: 8px 8px 0 0;
|
||
z-index: 50;
|
||
padding: 12px 20px;
|
||
margin: 0 -20px 15px -20px; /* Extend to edges of modal body */
|
||
}
|
||
|
||
.sorting-header-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 15px;
|
||
}
|
||
|
||
.sorting-status-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
|
||
.sorting-timer {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #2c5f76;
|
||
}
|
||
|
||
.sorting-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.header-btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.check-btn {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.check-btn:hover {
|
||
background: #218838;
|
||
}
|
||
|
||
.reveal-btn {
|
||
background: #ffc107;
|
||
color: #212529;
|
||
}
|
||
|
||
.reveal-btn:hover {
|
||
background: #e0a800;
|
||
}
|
||
|
||
.new-btn {
|
||
background: #007bff;
|
||
color: white;
|
||
}
|
||
|
||
.new-btn:hover {
|
||
background: #0056b3;
|
||
}
|
||
|
||
.end-btn {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.end-btn:hover {
|
||
background: #c82333;
|
||
}
|
||
|
||
.sorting-game-actions {
|
||
display: none; /* Hide old action buttons */
|
||
}
|
||
|
||
/* Score Modal Styles */
|
||
.score-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
z-index: 2000;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.score-modal-content {
|
||
background: white;
|
||
border-radius: 12px;
|
||
max-width: 600px;
|
||
width: 90%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
|
||
.score-modal-header {
|
||
padding: 20px 30px;
|
||
border-bottom: 1px solid #e1e5e9;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.score-modal-header h3 {
|
||
margin: 0;
|
||
color: #333;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.score-modal-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 32px;
|
||
cursor: pointer;
|
||
color: #666;
|
||
padding: 0;
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 20px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.score-modal-close:hover {
|
||
background: #f8f9fa;
|
||
color: #333;
|
||
}
|
||
|
||
.score-modal-body {
|
||
padding: 30px;
|
||
}
|
||
|
||
.score-modal-body .feedback-score {
|
||
font-size: 48px;
|
||
font-weight: bold;
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
color: #28a745;
|
||
}
|
||
|
||
.score-modal-body .feedback-message {
|
||
font-size: 20px;
|
||
text-align: center;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
}
|
||
|
||
.score-modal-body .feedback-time {
|
||
font-size: 18px;
|
||
text-align: center;
|
||
margin-bottom: 25px;
|
||
color: #666;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.score-modal-body .feedback-breakdown {
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.score-modal-body .score-component {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #e9ecef;
|
||
}
|
||
|
||
.score-modal-body .score-component:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.score-modal-body .component-label {
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.score-modal-body .component-score {
|
||
font-weight: bold;
|
||
color: #007bff;
|
||
}
|
||
|
||
.score-modal-body .component-weight {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.score-modal-body .feedback-details {
|
||
background: #e9ecef;
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
font-size: 16px;
|
||
color: #495057;
|
||
}
|
||
|
||
.score-modal-footer {
|
||
padding: 20px 30px;
|
||
border-top: 1px solid #e1e5e9;
|
||
display: flex;
|
||
gap: 15px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.modal-btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.new-game-btn {
|
||
background: #007bff;
|
||
color: white;
|
||
}
|
||
|
||
.new-game-btn:hover {
|
||
background: #0056b3;
|
||
}
|
||
|
||
.close-btn {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background: #545b62;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
transform: translateY(-50px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.sort-start-btn, .sort-check-btn, .sort-reveal-btn, .sort-new-btn {
|
||
background: #2c5f76;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 12px 24px;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
margin: 0 10px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.sort-start-btn:hover, .sort-check-btn:hover, .sort-reveal-btn:hover, .sort-new-btn:hover {
|
||
background: #1e4a61;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.sort-check-btn {
|
||
background: #28a745;
|
||
}
|
||
|
||
.sort-check-btn:hover {
|
||
background: #218838;
|
||
}
|
||
|
||
.sort-reveal-btn {
|
||
background: #ffc107;
|
||
color: #333;
|
||
}
|
||
|
||
.sort-reveal-btn:hover {
|
||
background: #e0a800;
|
||
}
|
||
|
||
.sort-new-btn {
|
||
background: #6f42c1;
|
||
}
|
||
|
||
.sort-new-btn:hover {
|
||
background: #5a32a3;
|
||
}
|
||
|
||
.sorting-instructions {
|
||
margin: 20px 0;
|
||
color: #2c5f76;
|
||
}
|
||
|
||
.sorting-progress {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 10px 20px;
|
||
margin: 10px 0;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
/* Horizontal layout for sorting game */
|
||
.sorting-game-layout {
|
||
display: flex;
|
||
gap: 30px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.available-cards-section {
|
||
flex: 1;
|
||
}
|
||
|
||
.sorting-slots-section {
|
||
flex: 2;
|
||
}
|
||
|
||
.available-cards-section h4,
|
||
.sorting-slots-section h4 {
|
||
margin: 0 0 15px 0;
|
||
color: #2c5f76;
|
||
font-size: 18px;
|
||
text-align: center;
|
||
}
|
||
|
||
.sorting-area {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
justify-content: center;
|
||
padding: 15px;
|
||
background: rgba(255,255,255,0.5);
|
||
border-radius: 8px;
|
||
min-height: 120px;
|
||
border: 2px dashed #2c5f76;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.sorting-game-layout {
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.available-cards-section,
|
||
.sorting-slots-section {
|
||
flex: none;
|
||
}
|
||
}
|
||
|
||
.position-slots {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 15px;
|
||
background: rgba(255,255,255,0.7);
|
||
border-radius: 8px;
|
||
border: 2px dashed #2c5f76;
|
||
}
|
||
|
||
.insert-button {
|
||
width: 32px;
|
||
height: 50px;
|
||
background: #2c5f76;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.insert-button:hover {
|
||
opacity: 1;
|
||
background: #1976d2;
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.insert-button.active {
|
||
opacity: 1;
|
||
background: #1976d2;
|
||
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
|
||
}
|
||
|
||
.insert-button.disabled {
|
||
opacity: 0.1;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.position-slot {
|
||
width: 90px;
|
||
height: 110px;
|
||
border: 2px solid #2c5f76;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
}
|
||
|
||
.position-slot.gradient-bg {
|
||
color: white;
|
||
border-color: rgba(255,255,255,0.3);
|
||
}
|
||
|
||
.position-slot:hover {
|
||
background: #f0f8ff;
|
||
border-color: #1a4a5c;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.position-slot.active {
|
||
background: #e3f2fd;
|
||
border-color: #1976d2;
|
||
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
|
||
}
|
||
|
||
|
||
.position-slot .slot-label {
|
||
font-size: 12px;
|
||
text-align: center;
|
||
font-weight: 500;
|
||
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
|
||
/* Color will be set dynamically based on background */
|
||
}
|
||
|
||
.position-slot.filled .slot-label {
|
||
position: absolute;
|
||
bottom: 8px;
|
||
left: 8px;
|
||
right: 8px;
|
||
color: #2c3e50;
|
||
text-shadow: none;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
}
|
||
|
||
.position-slot.filled:hover .slot-label {
|
||
opacity: 1;
|
||
}
|
||
|
||
.position-slot.filled {
|
||
background: #e8f5e8;
|
||
border-color: #28a745;
|
||
}
|
||
|
||
.position-slot.filled .slot-card {
|
||
position: absolute;
|
||
top: 5px;
|
||
left: 5px;
|
||
right: 5px;
|
||
bottom: 5px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.position-slot.correct {
|
||
border-color: #28a745;
|
||
background: #f8fff9;
|
||
}
|
||
|
||
.position-slot.incorrect {
|
||
border-color: #dc3545;
|
||
background: #fff8f8;
|
||
animation: shake 0.5s ease-in-out;
|
||
}
|
||
|
||
.sorting-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
gap: 15px;
|
||
margin: 30px 0;
|
||
padding: 20px;
|
||
background: rgba(255,255,255,0.7);
|
||
border-radius: 12px;
|
||
min-height: 300px;
|
||
}
|
||
|
||
.sort-card {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 8px;
|
||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||
cursor: pointer;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
border: 2px solid transparent;
|
||
position: relative;
|
||
user-select: none;
|
||
width: 90px;
|
||
height: 90px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.sort-card .card-svg svg {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
.sort-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||
border-color: #2c5f76;
|
||
}
|
||
|
||
.sort-card.selected {
|
||
border-color: #1976d2;
|
||
background: #e3f2fd;
|
||
transform: scale(1.1);
|
||
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.3);
|
||
}
|
||
|
||
.sort-card.placed {
|
||
opacity: 0.7;
|
||
transform: scale(0.9);
|
||
cursor: default;
|
||
}
|
||
|
||
.sort-card.placed:hover {
|
||
transform: scale(0.95);
|
||
border-color: #28a745;
|
||
}
|
||
|
||
.sort-card.correct {
|
||
border-color: #28a745;
|
||
background: #f8fff9;
|
||
}
|
||
|
||
.sort-card.incorrect {
|
||
border-color: #dc3545;
|
||
background: #fff8f8;
|
||
animation: shake 0.5s ease-in-out;
|
||
}
|
||
|
||
.sort-card .card-position {
|
||
position: absolute;
|
||
top: 5px;
|
||
left: 5px;
|
||
background: rgba(44, 95, 118, 0.8);
|
||
color: white;
|
||
border-radius: 50%;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.sort-card .revealed-number {
|
||
position: absolute;
|
||
top: 5px;
|
||
right: 5px;
|
||
background: #ffc107;
|
||
color: #333;
|
||
border-radius: 4px;
|
||
padding: 2px 8px;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
display: none;
|
||
}
|
||
|
||
.sort-card.revealed .revealed-number {
|
||
display: block;
|
||
}
|
||
|
||
.sorting-feedback {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin: 20px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.feedback-perfect {
|
||
background: linear-gradient(135deg, #d4edda, #c3e6cb);
|
||
color: #155724;
|
||
border: 2px solid #28a745;
|
||
}
|
||
|
||
.feedback-good {
|
||
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
|
||
color: #856404;
|
||
border: 2px solid #ffc107;
|
||
}
|
||
|
||
.feedback-needs-work {
|
||
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
|
||
color: #721c24;
|
||
border: 2px solid #dc3545;
|
||
}
|
||
|
||
.feedback-score {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.feedback-message {
|
||
font-size: 18px;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.feedback-details {
|
||
margin-top: 15px;
|
||
font-size: 14px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.feedback-breakdown {
|
||
margin: 20px 0;
|
||
padding: 15px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.score-component {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin: 8px 0;
|
||
padding: 5px 0;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.score-component:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.component-label {
|
||
font-weight: bold;
|
||
flex: 1;
|
||
}
|
||
|
||
.component-score {
|
||
font-weight: bold;
|
||
margin: 0 10px;
|
||
min-width: 40px;
|
||
text-align: right;
|
||
}
|
||
|
||
.component-weight {
|
||
font-size: 12px;
|
||
opacity: 0.7;
|
||
min-width: 80px;
|
||
text-align: right;
|
||
}
|
||
|
||
@keyframes shake {
|
||
0%, 100% { transform: translateX(0); }
|
||
25% { transform: translateX(-5px); }
|
||
75% { transform: translateX(5px); }
|
||
}
|
||
|
||
@keyframes success-pulse {
|
||
0% { transform: scale(1); }
|
||
50% { transform: scale(1.05); }
|
||
100% { transform: scale(1); }
|
||
}
|
||
|
||
.sort-card.success {
|
||
animation: success-pulse 0.6s ease-in-out;
|
||
}
|
||
|
||
/* Challenge Buttons */
|
||
.challenges-section {
|
||
margin: 40px 0;
|
||
max-width: 1200px;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
}
|
||
|
||
.section-header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.section-header h2 {
|
||
margin: 0 0 10px 0;
|
||
color: #2c3e50;
|
||
font-size: 28px;
|
||
}
|
||
|
||
.section-header p {
|
||
margin: 0;
|
||
color: #7a8695;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.challenges-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||
gap: 30px;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.challenge-category {
|
||
background: white;
|
||
border-radius: 16px;
|
||
padding: 25px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
border: 1px solid #f0f2f5;
|
||
}
|
||
|
||
.category-title {
|
||
margin: 0 0 20px 0;
|
||
color: #2c3e50;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
padding-bottom: 15px;
|
||
border-bottom: 2px solid #f0f2f5;
|
||
}
|
||
|
||
.challenge-cards {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.challenge-card {
|
||
background: linear-gradient(135deg, #4a90e2, #357abd);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
text-align: left;
|
||
min-height: 100px;
|
||
}
|
||
|
||
.challenge-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 25px rgba(74, 144, 226, 0.4);
|
||
}
|
||
|
||
.challenge-icon {
|
||
font-size: 32px;
|
||
flex-shrink: 0;
|
||
width: 50px;
|
||
height: 50px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255,255,255,0.2);
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.challenge-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.challenge-content h4 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.challenge-content p {
|
||
margin: 0 0 12px 0;
|
||
opacity: 0.9;
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.challenge-stats {
|
||
display: flex;
|
||
gap: 15px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.stat {
|
||
background: rgba(255,255,255,0.2);
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.sorting-card {
|
||
background: linear-gradient(135deg, #2c5f76, #1e4a61);
|
||
}
|
||
|
||
.sorting-card:hover {
|
||
box-shadow: 0 8px 25px rgba(44, 95, 118, 0.4);
|
||
}
|
||
|
||
.matching-card {
|
||
background: linear-gradient(135deg, #7b4397, #dc2430);
|
||
}
|
||
|
||
.matching-card:hover {
|
||
box-shadow: 0 8px 25px rgba(123, 67, 151, 0.4);
|
||
}
|
||
|
||
.start-game-btn {
|
||
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
|
||
color: white;
|
||
border: none;
|
||
padding: 15px 20px;
|
||
border-radius: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
text-align: center;
|
||
min-height: 70px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.start-game-btn:hover {
|
||
background: linear-gradient(135deg, #5f4ed6, #9085f5);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 20px rgba(108, 92, 231, 0.4);
|
||
}
|
||
|
||
.start-game-btn .btn-main {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.start-game-btn .btn-sub {
|
||
font-size: 13px;
|
||
opacity: 0.9;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.mode-buttons, .timer-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.mode-btn, .timer-btn {
|
||
background: #f8f9fa;
|
||
border: 2px solid #e9ecef;
|
||
padding: 10px 15px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 120px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.mode-btn.active, .timer-btn.active {
|
||
background: #007bff;
|
||
border-color: #007bff;
|
||
color: white;
|
||
}
|
||
|
||
.mode-btn:hover, .timer-btn:hover {
|
||
border-color: #007bff;
|
||
background: #e3f2fd;
|
||
}
|
||
|
||
.mode-btn.active:hover, .timer-btn.active:hover {
|
||
background: #0056b3;
|
||
}
|
||
|
||
.mode-icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.mode-text {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.timer-btn {
|
||
min-width: 80px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Matching Game Styles */
|
||
.matching-header {
|
||
position: sticky;
|
||
top: 0;
|
||
background: #f8f9fa;
|
||
border-bottom: 2px solid #e9ecef;
|
||
padding: 15px 20px;
|
||
z-index: 100;
|
||
margin: 0 -20px 15px -20px;
|
||
}
|
||
|
||
.matching-header-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.matching-status-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
|
||
.matching-stats {
|
||
display: flex;
|
||
gap: 15px;
|
||
font-size: 14px;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.matching-timer {
|
||
font-weight: 600;
|
||
color: #495057;
|
||
}
|
||
|
||
.player-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.player-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.player-info.active {
|
||
color: white;
|
||
}
|
||
|
||
.player-info.player1 {
|
||
border: 2px solid #007bff;
|
||
}
|
||
|
||
.player-info.player1.active {
|
||
background: #007bff;
|
||
}
|
||
|
||
.player-info.player2 {
|
||
border: 2px solid #fd7e14;
|
||
}
|
||
|
||
.player-info.player2.active {
|
||
background: #fd7e14;
|
||
}
|
||
|
||
.player-name {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.player-score {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.current-turn {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #007bff;
|
||
text-align: center;
|
||
}
|
||
|
||
.turn-timer {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #495057;
|
||
background: #e9ecef;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
min-width: 60px;
|
||
text-align: center;
|
||
border: 2px solid #dee2e6;
|
||
}
|
||
|
||
.turn-timer.warning {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
animation: timerPulse 1s infinite;
|
||
}
|
||
|
||
.turn-timer.critical {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
animation: timerPulse 0.5s infinite;
|
||
}
|
||
|
||
.turn-timer.waiting {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
@keyframes timerPulse {
|
||
0%, 100% { transform: scale(1); }
|
||
50% { transform: scale(1.1); }
|
||
}
|
||
|
||
.two-player-results {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 30px;
|
||
margin: 20px 0;
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.player-result {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.player-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
}
|
||
|
||
.player-final-score {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #007bff;
|
||
}
|
||
|
||
.vs-divider {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #6c757d;
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.game-summary {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background: #e3f2fd;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.summary-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.summary-label {
|
||
font-size: 12px;
|
||
color: #6c757d;
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.summary-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
}
|
||
|
||
.matching-grid {
|
||
display: grid;
|
||
gap: 8px;
|
||
padding: 10px;
|
||
justify-content: center;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.matching-grid.grid-3x4 {
|
||
grid-template-columns: repeat(4, 1fr);
|
||
aspect-ratio: 4/3;
|
||
width: min(100%, calc(100vh - 280px) * 4/3);
|
||
height: min(calc(100vw - 40px) * 3/4, calc(100vh - 280px));
|
||
}
|
||
|
||
.matching-grid.grid-4x4 {
|
||
grid-template-columns: repeat(4, 1fr);
|
||
aspect-ratio: 1;
|
||
width: min(100%, calc(100vh - 280px));
|
||
height: min(calc(100vw - 40px), calc(100vh - 280px));
|
||
}
|
||
|
||
.matching-grid.grid-4x6 {
|
||
grid-template-columns: repeat(6, 1fr);
|
||
aspect-ratio: 6/4;
|
||
width: min(100%, calc(100vh - 280px) * 6/4);
|
||
height: min(calc(100vw - 40px) * 4/6, calc(100vh - 280px));
|
||
}
|
||
|
||
.matching-grid.grid-5x6 {
|
||
grid-template-columns: repeat(6, 1fr);
|
||
aspect-ratio: 6/5;
|
||
width: min(100%, calc(100vh - 280px) * 6/5);
|
||
height: min(calc(100vw - 40px) * 5/6, calc(100vh - 280px));
|
||
}
|
||
|
||
.match-card {
|
||
aspect-ratio: 1;
|
||
background: #ffffff;
|
||
border: 2px solid #e9ecef;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
user-select: none;
|
||
width: 100%;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* Cards automatically size to fill grid cells while maintaining aspect ratio */
|
||
.match-card {
|
||
/* Let the card fill the grid cell */
|
||
width: 100%;
|
||
height: 100%;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
}
|
||
|
||
.match-card:hover {
|
||
border-color: #007bff;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
|
||
}
|
||
|
||
.match-card.flipped {
|
||
background: #f8f9fa;
|
||
border-color: #007bff;
|
||
}
|
||
|
||
.match-card.matched {
|
||
background: #d4edda;
|
||
border-color: #28a745;
|
||
cursor: default;
|
||
position: relative;
|
||
opacity: 0.7;
|
||
filter: grayscale(20%);
|
||
transform: scale(0.95);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.match-card.matched:hover {
|
||
transform: scale(0.95);
|
||
box-shadow: none;
|
||
border-color: #28a745;
|
||
}
|
||
|
||
.match-card.matched-player1 {
|
||
background: #cce7ff;
|
||
border-color: #007bff;
|
||
box-shadow: none;
|
||
opacity: 0.7;
|
||
filter: grayscale(20%);
|
||
}
|
||
|
||
.match-card.matched-player1:hover {
|
||
border-color: #007bff;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.match-card.matched-player1::before {
|
||
border-color: #007bff !important;
|
||
background: linear-gradient(45deg, rgba(0, 123, 255, 0.1), rgba(0, 123, 255, 0.05)) !important;
|
||
}
|
||
|
||
.match-card.matched-player1::after {
|
||
background: #007bff !important;
|
||
box-shadow: 0 0 0 1px #007bff, 0 0 8px rgba(0, 123, 255, 0.4) !important;
|
||
}
|
||
|
||
.match-card.matched-player2 {
|
||
background: #ffe6cc;
|
||
border-color: #fd7e14;
|
||
box-shadow: none;
|
||
opacity: 0.7;
|
||
filter: grayscale(20%);
|
||
}
|
||
|
||
.match-card.matched-player2:hover {
|
||
border-color: #fd7e14;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.match-card.matched-player2::before {
|
||
border-color: #fd7e14 !important;
|
||
background: linear-gradient(45deg, rgba(253, 126, 20, 0.1), rgba(253, 126, 20, 0.05)) !important;
|
||
}
|
||
|
||
.match-card.matched-player2::after {
|
||
background: #fd7e14 !important;
|
||
box-shadow: 0 0 0 1px #fd7e14, 0 0 8px rgba(253, 126, 20, 0.4) !important;
|
||
}
|
||
|
||
.player-badge {
|
||
position: absolute;
|
||
top: -8px;
|
||
right: -8px;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: white;
|
||
border: 2px solid white;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
z-index: 20;
|
||
}
|
||
|
||
.player-badge.player1 {
|
||
background: #007bff;
|
||
}
|
||
|
||
.player-badge.player2 {
|
||
background: #fd7e14;
|
||
}
|
||
|
||
.match-card.matched::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 3px;
|
||
left: 3px;
|
||
width: 12px;
|
||
height: 12px;
|
||
background: #28a745;
|
||
border: 2px solid white;
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 0 1px #28a745, 0 0 8px rgba(40, 167, 69, 0.4);
|
||
pointer-events: none;
|
||
z-index: 10;
|
||
}
|
||
|
||
.match-card.matched::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -2px;
|
||
left: -2px;
|
||
right: -2px;
|
||
bottom: -2px;
|
||
border: 2px solid #28a745;
|
||
border-radius: 10px;
|
||
background: linear-gradient(45deg, rgba(40, 167, 69, 0.1), rgba(40, 167, 69, 0.05));
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
}
|
||
|
||
.match-card.matched .match-card-content {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.match-card.invalid-move {
|
||
animation: invalidMoveShake 0.6s ease-in-out;
|
||
border-color: #dc3545 !important;
|
||
}
|
||
|
||
@keyframes invalidMoveShake {
|
||
0%, 100% { transform: translateX(0); }
|
||
10%, 30%, 50%, 70%, 90% { transform: translateX(-3px); }
|
||
20%, 40%, 60%, 80% { transform: translateX(3px); }
|
||
}
|
||
|
||
.match-card.valid-choice {
|
||
border-color: #28a745 !important;
|
||
box-shadow: 0 0 8px rgba(40, 167, 69, 0.4);
|
||
animation: validChoicePulse 2s infinite;
|
||
}
|
||
|
||
.match-card.invalid-choice {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.match-card.invalid-choice:hover {
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
@keyframes validChoicePulse {
|
||
0%, 100% { box-shadow: 0 0 8px rgba(40, 167, 69, 0.4); }
|
||
50% { box-shadow: 0 0 12px rgba(40, 167, 69, 0.7); }
|
||
}
|
||
|
||
@keyframes matchedCard {
|
||
0% {
|
||
transform: scale(1) rotate(0deg);
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
transform: scale(1.1) rotate(2deg);
|
||
opacity: 0.9;
|
||
}
|
||
100% {
|
||
transform: scale(0.95) rotate(0deg);
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
.match-card.matched {
|
||
animation: matchedCard 0.6s ease-out forwards;
|
||
}
|
||
|
||
.match-card-content {
|
||
display: none;
|
||
width: 100%;
|
||
height: 100%;
|
||
padding: 8px;
|
||
}
|
||
|
||
.match-card.flipped .match-card-content,
|
||
.match-card.matched .match-card-content {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.match-card-number {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #495057;
|
||
}
|
||
|
||
.match-card-abacus {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.match-card-abacus svg {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.match-card-back {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
gap: 4px;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.match-card-back.abacus-type {
|
||
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
|
||
}
|
||
|
||
.match-card-back.number-type {
|
||
background: linear-gradient(135deg, #00b894, #00cec9);
|
||
}
|
||
|
||
.match-card-back .card-type-icon {
|
||
font-size: 40px;
|
||
}
|
||
|
||
.match-card.flipped .match-card-back,
|
||
.match-card.matched .match-card-back {
|
||
display: none;
|
||
}
|
||
|
||
.matching-instructions {
|
||
background: #e3f2fd;
|
||
border: 1px solid #bbdefb;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
margin: 0 0 20px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.matching-feedback {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin: 20px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.matching-grid {
|
||
gap: 6px;
|
||
padding: 8px;
|
||
}
|
||
|
||
.matching-grid.grid-3x4 {
|
||
width: min(100%, calc(100vh - 260px) * 4/3);
|
||
height: min(calc(100vw - 30px) * 3/4, calc(100vh - 260px));
|
||
}
|
||
|
||
.matching-grid.grid-4x4 {
|
||
width: min(100%, calc(100vh - 260px));
|
||
height: min(calc(100vw - 30px), calc(100vh - 260px));
|
||
}
|
||
|
||
.matching-grid.grid-4x6 {
|
||
width: min(100%, calc(100vh - 260px) * 6/4);
|
||
height: min(calc(100vw - 30px) * 4/6, calc(100vh - 260px));
|
||
}
|
||
|
||
.matching-grid.grid-5x6 {
|
||
width: min(100%, calc(100vh - 260px) * 6/5);
|
||
height: min(calc(100vw - 30px) * 5/6, calc(100vh - 260px));
|
||
}
|
||
|
||
.match-card-number {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.match-card-back .card-type-icon {
|
||
font-size: 28px;
|
||
}
|
||
|
||
.matching-header {
|
||
padding: 10px 15px;
|
||
margin: 0 -20px 10px -20px;
|
||
}
|
||
|
||
.matching-instructions {
|
||
padding: 12px;
|
||
margin: 0 0 15px 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.start-game-btn {
|
||
min-height: 60px;
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.start-game-btn .btn-main {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.start-game-btn .btn-sub {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.matching-grid {
|
||
gap: 4px;
|
||
padding: 5px;
|
||
}
|
||
|
||
.matching-grid.grid-3x4 {
|
||
width: min(100%, calc(100vh - 240px) * 4/3);
|
||
height: min(calc(100vw - 20px) * 3/4, calc(100vh - 240px));
|
||
}
|
||
|
||
.matching-grid.grid-4x4 {
|
||
width: min(100%, calc(100vh - 240px));
|
||
height: min(calc(100vw - 20px), calc(100vh - 240px));
|
||
}
|
||
|
||
.matching-grid.grid-4x6 {
|
||
width: min(100%, calc(100vh - 240px) * 6/4);
|
||
height: min(calc(100vw - 20px) * 4/6, calc(100vh - 240px));
|
||
}
|
||
|
||
.matching-grid.grid-5x6 {
|
||
width: min(100%, calc(100vh - 240px) * 6/5);
|
||
height: min(calc(100vw - 20px) * 5/6, calc(100vh - 240px));
|
||
}
|
||
|
||
.match-card-number {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.match-card-back .card-type-icon {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.start-game-btn {
|
||
min-height: 50px;
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.start-game-btn .btn-main {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.start-game-btn .btn-sub {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.match-card.matched::after {
|
||
width: 10px;
|
||
height: 10px;
|
||
bottom: 2px;
|
||
left: 2px;
|
||
}
|
||
}
|
||
|
||
/* Modal Styling */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 1000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
animation: fadeIn 0.3s ease-out;
|
||
}
|
||
|
||
.modal.show {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
border-radius: 12px;
|
||
width: 90%;
|
||
max-width: 900px;
|
||
max-height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
animation: slideIn 0.3s ease-out;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px 30px;
|
||
border-bottom: 1px solid #eee;
|
||
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||
border-radius: 12px 12px 0 0;
|
||
}
|
||
|
||
.modal-header h2 {
|
||
margin: 0;
|
||
color: #333;
|
||
}
|
||
|
||
.modal-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.fullscreen-btn, .close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
padding: 5px 10px;
|
||
border-radius: 6px;
|
||
transition: background 0.2s ease;
|
||
}
|
||
|
||
.fullscreen-btn:hover, .close-btn:hover {
|
||
background: rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.close-btn {
|
||
color: #666;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
color: #333;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 30px;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* Fullscreen modal styling */
|
||
.modal.fullscreen {
|
||
background: rgba(0, 0, 0, 0.95);
|
||
}
|
||
|
||
.modal.fullscreen .modal-content {
|
||
width: 100%;
|
||
height: 100%;
|
||
max-width: none;
|
||
max-height: none;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.modal.fullscreen .modal-header {
|
||
border-radius: 0;
|
||
}
|
||
|
||
/* Animations */
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from { transform: scale(0.9) translateY(-20px); opacity: 0; }
|
||
to { transform: scale(1) translateY(0); opacity: 1; }
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<!-- Main Page Header -->
|
||
<div class="header">
|
||
<h1>Soroban Flashcards</h1>
|
||
<p>Interactive learning tools for mastering the Japanese abacus</p>
|
||
</div>
|
||
|
||
<!-- Navigation -->
|
||
<nav class="page-nav">
|
||
<div class="nav-buttons">
|
||
<button class="nav-btn active" data-section="introduction">📚 Introduction</button>
|
||
<button class="nav-btn" data-section="configuration">⚙️ Configuration</button>
|
||
<button class="nav-btn" data-section="challenges">🎯 Challenges</button>
|
||
<button class="nav-btn" data-section="flashcards">🧮 Flashcards</button>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Section 1: Introduction -->
|
||
<section id="introduction" class="page-section active">
|
||
<div class="section-header">
|
||
<h2 class="section-title">📚 How to Learn with Soroban</h2>
|
||
<p class="section-subtitle">Master the Japanese abacus through interactive practice</p>
|
||
</div>
|
||
|
||
<div class="instructions">
|
||
<h3>Understanding the Soroban:</h3>
|
||
<p>The soroban (Japanese abacus) represents numbers using beads arranged in columns. Each column represents a place value (ones, tens, hundreds, etc.). In each column:</p>
|
||
<ul style="text-align: left; display: inline-block; margin: 20px 0;">
|
||
<li><strong>Top bead (heaven):</strong> Represents 5</li>
|
||
<li><strong>Bottom beads (earth):</strong> Each represents 1</li>
|
||
<li><strong>Active beads:</strong> Pushed toward the horizontal bar</li>
|
||
</ul>
|
||
|
||
<h3>How to Practice:</h3>
|
||
<p>Look at each abacus representation and try to determine the number before hovering or clicking to reveal the answer. Start with smaller numbers and work your way up to more complex calculations.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Section 2: Configuration -->
|
||
<section id="configuration" class="page-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">⚙️ Current Configuration</h2>
|
||
<p class="section-subtitle">Settings used for this flashcard set</p>
|
||
</div>
|
||
|
||
<div class="stats">
|
||
<div><strong>Cards:</strong> 10</div>
|
||
<div><strong>Range:</strong> 1 - 10</div>
|
||
<div><strong>Color Scheme:</strong> All beads are the same color</div>
|
||
<div><strong>Bead Shape:</strong> Diamond</div>
|
||
</div>
|
||
|
||
<div class="instructions">
|
||
<h3>Configuration Details:</h3>
|
||
<p>This flashcard set has been generated with specific settings to help you practice. You can generate new sets with different ranges, color schemes, and visual styles using the command-line tools.</p>
|
||
|
||
<h4>Available Options:</h4>
|
||
<ul style="text-align: left; display: inline-block; margin: 20px 0;">
|
||
<li><strong>Number Ranges:</strong> Practice with any range from 0-99999</li>
|
||
<li><strong>Color Schemes:</strong> Monochrome, place-value, heaven-earth, alternating</li>
|
||
<li><strong>Bead Shapes:</strong> Diamond, circle, square</li>
|
||
<li><strong>Visual Styles:</strong> Multiple colorblind-friendly palettes</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Section 3: Challenges -->
|
||
<section id="challenges" class="page-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">🎯 Interactive Challenges</h2>
|
||
<p class="section-subtitle">Test your soroban skills with engaging games and quizzes</p>
|
||
</div>
|
||
|
||
<div class="challenges-section">
|
||
|
||
<div class="challenges-grid">
|
||
<div class="challenge-category">
|
||
<h3 class="category-title">🧠 Memory & Speed</h3>
|
||
<div class="challenge-cards">
|
||
<button id="open-quiz-modal" class="challenge-card quiz-card">
|
||
<div class="challenge-icon">⚡</div>
|
||
<div class="challenge-content">
|
||
<h4>Speed Memory Quiz</h4>
|
||
<p>Quick card displays test your reading skills</p>
|
||
<div class="challenge-stats">
|
||
<span class="stat">🎯 Accuracy</span>
|
||
<span class="stat">⏱️ Speed</span>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="challenge-category">
|
||
<h3 class="category-title">🎲 Logic & Patterns</h3>
|
||
<div class="challenge-cards">
|
||
<button id="open-sorting-modal" class="challenge-card sorting-card">
|
||
<div class="challenge-icon">🔢</div>
|
||
<div class="challenge-content">
|
||
<h4>Card Sorting Challenge</h4>
|
||
<p>Arrange cards using only abacus patterns</p>
|
||
<div class="challenge-stats">
|
||
<span class="stat">🧩 Logic</span>
|
||
<span class="stat">👀 Pattern Recognition</span>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
|
||
<button id="open-matching-modal" class="challenge-card matching-card">
|
||
<div class="challenge-icon">🧩</div>
|
||
<div class="challenge-content">
|
||
<h4>Matching Pairs</h4>
|
||
<p>Match abacus patterns with their numerals</p>
|
||
<div class="challenge-stats">
|
||
<span class="stat">🔍 Memory</span>
|
||
<span class="stat">⚡ Speed</span>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quiz Modal -->
|
||
<div id="quiz-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Speed Memory Quiz</h2>
|
||
<div class="modal-controls">
|
||
<button id="quiz-fullscreen-btn" class="fullscreen-btn" title="Toggle Fullscreen">⛶</button>
|
||
<button id="close-quiz-modal" class="close-btn">×</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>Test your soroban reading skills! Cards will be shown briefly, then you'll enter the numbers you remember.</p>
|
||
|
||
<div class="quiz-controls">
|
||
<div class="control-group">
|
||
<label for="quiz-count">Cards to Quiz:</label>
|
||
<div class="count-buttons">
|
||
<button type="button" class="count-btn" data-count="5">5</button>
|
||
<button type="button" class="count-btn" data-count="10">10</button>
|
||
<button type="button" class="count-btn active" data-count="15">15</button>
|
||
<button type="button" class="count-btn" data-count="25">25</button>
|
||
<button type="button" class="count-btn" data-count="all">All (10)</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="control-group">
|
||
<label for="display-time">Display Time per Card:</label>
|
||
<div class="slider-container">
|
||
<input type="range" id="display-time" min="0.5" max="10" step="0.5" value="2" />
|
||
<span class="slider-value">2.0s</span>
|
||
</div>
|
||
</div>
|
||
|
||
<button id="start-quiz" class="quiz-start-btn">Start Quiz</button>
|
||
</div>
|
||
|
||
<!-- Quiz Game Area (hidden initially) -->
|
||
<div id="quiz-game" class="quiz-game" style="display: none;">
|
||
<div class="quiz-header">
|
||
<div class="quiz-progress">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill"></div>
|
||
</div>
|
||
<span class="progress-text">Card <span id="current-card">1</span> of <span id="total-cards">10</span></span>
|
||
</div>
|
||
<button id="end-quiz" class="end-game-btn">End Quiz</button>
|
||
</div>
|
||
|
||
<div class="quiz-display">
|
||
<div id="quiz-card" class="quiz-flashcard">
|
||
<!-- Card content will be inserted here -->
|
||
</div>
|
||
<div id="quiz-countdown" class="countdown">Get Ready...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quiz Input Phase (hidden initially) -->
|
||
<div id="quiz-input" class="quiz-input" style="display: none;">
|
||
<h3>Enter the Numbers You Remember</h3>
|
||
<div class="quiz-stats">
|
||
<div class="stats-item">
|
||
<span class="stats-label">Cards shown:</span>
|
||
<span id="cards-shown-count">0</span>
|
||
</div>
|
||
<div class="stats-item">
|
||
<span class="stats-label">Guesses left:</span>
|
||
<span id="guesses-remaining">0</span>
|
||
</div>
|
||
<div class="stats-item">
|
||
<span class="stats-label">Found:</span>
|
||
<span id="numbers-found">0</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="smart-input-container">
|
||
<div class="smart-input-prompt">Type the numbers you remember:</div>
|
||
<div class="number-display" id="number-display">
|
||
<span class="current-typing" id="current-typing"></span>
|
||
</div>
|
||
<input type="text" id="smart-input" style="position: absolute; left: -9999px; opacity: 0; pointer-events: none;" autocomplete="off">
|
||
</div>
|
||
|
||
<div class="found-numbers" id="found-numbers">
|
||
<!-- Accepted numbers will appear here -->
|
||
</div>
|
||
|
||
<div class="quiz-finish-buttons">
|
||
<button id="finish-quiz" class="finish-btn" style="display: none;">Finish Quiz</button>
|
||
<button id="give-up-quiz" class="give-up-btn" style="display: none;">Can't Remember More</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quiz Results (hidden initially) -->
|
||
<div id="quiz-results" class="quiz-results" style="display: none;">
|
||
<h3>Quiz Results</h3>
|
||
<div class="score-display">
|
||
<div class="score-circle">
|
||
<span id="score-percentage">0%</span>
|
||
</div>
|
||
<div class="score-details">
|
||
<p><strong>Score:</strong> <span id="score-correct">0</span> / <span id="score-total">0</span> correct</p>
|
||
<p><strong>Time per card:</strong> <span id="result-timing">2.0s</span></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="results-breakdown">
|
||
<h4>Detailed Results:</h4>
|
||
<div id="results-list">
|
||
<!-- Results will be inserted here -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-actions">
|
||
<button id="retry-quiz">Try Again</button>
|
||
<button id="back-to-cards">Back to Cards</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sorting Modal -->
|
||
<div id="sorting-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Card Sorting Challenge</h2>
|
||
<div class="modal-controls">
|
||
<button id="sorting-fullscreen-btn" class="fullscreen-btn" title="Toggle Fullscreen">⛶</button>
|
||
<button id="close-sorting-modal" class="close-btn">×</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>Click cards and positions to arrange them in ascending order (smallest to largest). No numerals shown - rely on reading the abacus!</p>
|
||
|
||
<div class="sorting-controls">
|
||
<div class="control-group">
|
||
<label for="sort-count">Cards to Sort:</label>
|
||
<div class="count-buttons">
|
||
<button type="button" class="sort-count-btn active" data-count="5">5</button>
|
||
<button type="button" class="sort-count-btn" data-count="8">8</button>
|
||
<button type="button" class="sort-count-btn" data-count="12">12</button>
|
||
<button type="button" class="sort-count-btn" data-count="15">15</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sorting-actions">
|
||
<button id="start-sorting" class="sort-start-btn">Start Sorting Challenge</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Game Action Buttons (now moved to sticky header) -->
|
||
<div class="sorting-game-actions" style="display: none;">
|
||
<!-- Buttons moved to sticky header -->
|
||
</div>
|
||
|
||
<!-- Sorting Game Header (Sticky) -->
|
||
<div id="sorting-header" class="sorting-header" style="display: none;">
|
||
<div class="sorting-header-content">
|
||
<div class="sorting-status-group">
|
||
<span id="sorting-status">Ready to start</span>
|
||
<div class="sorting-timer" id="sorting-timer">0:00</div>
|
||
</div>
|
||
<div class="sorting-controls">
|
||
<button id="check-sorting" class="header-btn check-btn">Check Solution</button>
|
||
<button id="reveal-numbers" class="header-btn reveal-btn">Show Numbers</button>
|
||
<button id="new-sorting" class="header-btn new-btn">New Challenge</button>
|
||
<button id="end-sorting" class="header-btn end-btn">End Game</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sorting Game Area -->
|
||
<div id="sorting-game" style="display: none;">
|
||
<div class="sorting-instructions">
|
||
<p><strong>Instructions:</strong> Click a card → Click position or + button to place. Click placed cards to move back.</p>
|
||
</div>
|
||
|
||
<div class="sorting-game-layout">
|
||
<div class="available-cards-section">
|
||
<h4>Available Cards</h4>
|
||
<div id="sorting-area" class="sorting-area">
|
||
<!-- Available cards will be shown here -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sorting-slots-section">
|
||
<h4>Sorting Positions (Smallest → Largest)</h4>
|
||
<div id="position-slots" class="position-slots">
|
||
<!-- Position slots will be created here -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sorting-feedback" id="sorting-feedback" style="display: none;">
|
||
<!-- Feedback will be shown here -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Score Report Modal -->
|
||
<div id="score-modal" class="score-modal" style="display: none;">
|
||
<div class="score-modal-content">
|
||
<div class="score-modal-header">
|
||
<h3>🎯 Sorting Challenge Results</h3>
|
||
<button class="score-modal-close">×</button>
|
||
</div>
|
||
<div id="score-modal-body" class="score-modal-body">
|
||
<!-- Score content will be inserted here -->
|
||
</div>
|
||
<div class="score-modal-footer">
|
||
<button id="score-modal-new-game" class="modal-btn new-game-btn">New Challenge</button>
|
||
<button id="score-modal-close-btn" class="modal-btn close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Matching Pairs Modal -->
|
||
<div id="matching-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Matching Pairs Challenge</h2>
|
||
<div class="modal-controls">
|
||
<button id="matching-fullscreen-btn" class="fullscreen-btn" title="Toggle Fullscreen">⛶</button>
|
||
<button id="close-matching-modal" class="close-btn">×</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>Match abacus patterns with their corresponding numerals. Find all pairs in minimum moves!</p>
|
||
|
||
<div class="matching-controls">
|
||
<div class="control-group">
|
||
<label>Game Mode:</label>
|
||
<div class="mode-buttons">
|
||
<button type="button" class="mode-btn active" data-mode="single">
|
||
<div class="mode-icon">👤</div>
|
||
<div class="mode-text">Single Player</div>
|
||
</button>
|
||
<button type="button" class="mode-btn" data-mode="two-player">
|
||
<div class="mode-icon">👥</div>
|
||
<div class="mode-text">Two Player</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="control-group" id="timer-controls" style="display: none;">
|
||
<label>Turn Timer:</label>
|
||
<div class="timer-buttons">
|
||
<button type="button" class="timer-btn active" data-timer="0">No Timer</button>
|
||
<button type="button" class="timer-btn" data-timer="15">15 sec</button>
|
||
<button type="button" class="timer-btn" data-timer="30">30 sec</button>
|
||
<button type="button" class="timer-btn" data-timer="60">60 sec</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="control-group">
|
||
<label for="match-grid-size">Choose Grid Size to Start:</label>
|
||
<div class="count-buttons">
|
||
<button type="button" class="match-size-btn start-game-btn" data-pairs="6">
|
||
<div class="btn-main">3×4 Grid</div>
|
||
<div class="btn-sub">6 pairs</div>
|
||
</button>
|
||
<button type="button" class="match-size-btn start-game-btn" data-pairs="8">
|
||
<div class="btn-main">4×4 Grid</div>
|
||
<div class="btn-sub">8 pairs</div>
|
||
</button>
|
||
<button type="button" class="match-size-btn start-game-btn" data-pairs="12">
|
||
<div class="btn-main">4×6 Grid</div>
|
||
<div class="btn-sub">12 pairs</div>
|
||
</button>
|
||
<button type="button" class="match-size-btn start-game-btn" data-pairs="15">
|
||
<div class="btn-main">5×6 Grid</div>
|
||
<div class="btn-sub">15 pairs</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Matching Game Header (Sticky) -->
|
||
<div id="matching-header" class="matching-header" style="display: none;">
|
||
<div class="matching-header-content">
|
||
<div class="matching-status-group">
|
||
<span id="matching-status">Ready to start</span>
|
||
<div class="matching-stats">
|
||
<span id="moves-counter">Moves: 0</span>
|
||
<span id="pairs-found">Pairs: 0/0</span>
|
||
<div class="matching-timer" id="matching-timer">0:00</div>
|
||
</div>
|
||
<!-- Two-player specific stats -->
|
||
<div class="player-stats" id="player-stats" style="display: none;">
|
||
<div class="player-info player1" id="player1-info">
|
||
<span class="player-name">🔵 Player 1</span>
|
||
<span class="player-score">0 pairs</span>
|
||
</div>
|
||
<div class="current-turn" id="current-turn">🔵 Player 1's Turn</div>
|
||
<div class="turn-timer" id="turn-timer" style="display: none;">30s</div>
|
||
<div class="player-info player2" id="player2-info">
|
||
<span class="player-name">🟠 Player 2</span>
|
||
<span class="player-score">0 pairs</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="matching-controls">
|
||
<button id="new-matching" class="header-btn new-btn">New Game</button>
|
||
<button id="end-matching" class="header-btn end-btn">End Game</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Matching Game Area -->
|
||
<div id="matching-game" style="display: none;">
|
||
<div class="matching-instructions">
|
||
<p><strong>Instructions:</strong> Click two cards to flip them. Purple cards (🧮) contain abacus patterns, green cards (🔢) contain numbers. Match abacus patterns with their corresponding numbers. Complete all pairs with minimum moves!</p>
|
||
</div>
|
||
|
||
<div id="matching-grid" class="matching-grid">
|
||
<!-- Matching cards will be generated here -->
|
||
</div>
|
||
|
||
<div class="matching-feedback" id="matching-feedback" style="display: none;">
|
||
<!-- Feedback will be shown here -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Matching Score Modal -->
|
||
<div id="matching-score-modal" class="score-modal" style="display: none;">
|
||
<div class="score-modal-content">
|
||
<div class="score-modal-header">
|
||
<h3>🧩 Matching Pairs Results</h3>
|
||
<button class="matching-score-modal-close">×</button>
|
||
</div>
|
||
<div id="matching-score-modal-body" class="score-modal-body">
|
||
<!-- Score content will be inserted here -->
|
||
</div>
|
||
<div class="score-modal-footer">
|
||
<button id="matching-score-modal-new-game" class="modal-btn new-game-btn">New Challenge</button>
|
||
<button id="matching-score-modal-close-btn" class="modal-btn close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
|
||
<!-- Section 4: Flashcards -->
|
||
<section id="flashcards" class="page-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">🧮 Practice Flashcards</h2>
|
||
<p class="section-subtitle">Interactive cards for hands-on learning</p>
|
||
</div>
|
||
|
||
<div class="cards-grid" id="cards-grid">
|
||
|
||
<div class="flashcard" data-number="1">
|
||
<div class="card-number">#1</div>
|
||
<div class="abacus-container">
|
||
<svg class="typst-doc" viewBox="0 0 108 108" width="108pt" height="108pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||
<g>
|
||
<g transform="translate(5.400000000000001 5.400000000000001)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(36.1 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 -11.400000000000002)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(11 10)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 80 L 3 80 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 10)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 33)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 53)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 65.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 78)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(0 30)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="numeral">1</div>
|
||
</div>
|
||
<div class="flashcard" data-number="2">
|
||
<div class="card-number">#2</div>
|
||
<div class="abacus-container">
|
||
<svg class="typst-doc" viewBox="0 0 108 108" width="108pt" height="108pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||
<g>
|
||
<g transform="translate(5.400000000000001 5.400000000000001)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(36.1 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 -11.400000000000002)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(11 10)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 80 L 3 80 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 10)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 33)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 45.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 65.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 78)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(0 30)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="numeral">2</div>
|
||
</div>
|
||
<div class="flashcard" data-number="3">
|
||
<div class="card-number">#3</div>
|
||
<div class="abacus-container">
|
||
<svg class="typst-doc" viewBox="0 0 108 108" width="108pt" height="108pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||
<g>
|
||
<g transform="translate(5.400000000000001 5.400000000000001)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(36.1 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 -11.400000000000002)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(11 10)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 80 L 3 80 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 10)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 33)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 45.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 58)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 78)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(0 30)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="numeral">3</div>
|
||
</div>
|
||
<div class="flashcard" data-number="4">
|
||
<div class="card-number">#4</div>
|
||
<div class="abacus-container">
|
||
<svg class="typst-doc" viewBox="0 0 108 108" width="108pt" height="108pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||
<g>
|
||
<g transform="translate(5.400000000000001 5.400000000000001)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(36.1 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 -11.400000000000002)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(11 10)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 72.5 L 3 72.5 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 10)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 33)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 45.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 58)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 70.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(0 30)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="numeral">4</div>
|
||
</div>
|
||
<div class="flashcard" data-number="5">
|
||
<div class="card-number">#5</div>
|
||
<div class="abacus-container">
|
||
<svg class="typst-doc" viewBox="0 0 108 108" width="108pt" height="108pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||
<g>
|
||
<g transform="translate(5.400000000000001 5.400000000000001)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(36.1 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 -11.400000000000002)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(11 17)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 72.5 L 3 72.5 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 17)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 40)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 52.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 65)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 77.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(0 30)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="numeral">5</div>
|
||
</div>
|
||
<div class="flashcard" data-number="6">
|
||
<div class="card-number">#6</div>
|
||
<div class="abacus-container">
|
||
<svg class="typst-doc" viewBox="0 0 108 108" width="108pt" height="108pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||
<g>
|
||
<g transform="translate(5.400000000000001 5.400000000000001)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(36.1 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 -11.400000000000002)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(11 17)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 73 L 3 73 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 17)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 33)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 53)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 65.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 78)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(0 30)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="numeral">6</div>
|
||
</div>
|
||
<div class="flashcard" data-number="7">
|
||
<div class="card-number">#7</div>
|
||
<div class="abacus-container">
|
||
<svg class="typst-doc" viewBox="0 0 108 108" width="108pt" height="108pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||
<g>
|
||
<g transform="translate(5.400000000000001 5.400000000000001)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(36.1 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 -11.400000000000002)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(11 17)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 73 L 3 73 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 17)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 33)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 45.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 65.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 78)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(0 30)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="numeral">7</div>
|
||
</div>
|
||
<div class="flashcard" data-number="8">
|
||
<div class="card-number">#8</div>
|
||
<div class="abacus-container">
|
||
<svg class="typst-doc" viewBox="0 0 108 108" width="108pt" height="108pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||
<g>
|
||
<g transform="translate(5.400000000000001 5.400000000000001)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(36.1 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 -11.400000000000002)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(11 17)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 73 L 3 73 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 17)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 33)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 45.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 58)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 78)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(0 30)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="numeral">8</div>
|
||
</div>
|
||
<div class="flashcard" data-number="9">
|
||
<div class="card-number">#9</div>
|
||
<div class="abacus-container">
|
||
<svg class="typst-doc" viewBox="0 0 108 108" width="108pt" height="108pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||
<g>
|
||
<g transform="translate(5.400000000000001 5.400000000000001)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(36.1 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 -11.400000000000002)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(11 17)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 65.5 L 3 65.5 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 17)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 33)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 45.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 58)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 70.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(0 30)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 25 2 L 25 0 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="numeral">9</div>
|
||
</div>
|
||
<div class="flashcard" data-number="10">
|
||
<div class="card-number">#10</div>
|
||
<div class="abacus-container">
|
||
<svg class="typst-doc" viewBox="0 0 108 108" width="108pt" height="108pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||
<g>
|
||
<g transform="translate(5.400000000000001 5.400000000000001)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(23.599999999999998 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 -11.400000000000002)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(11 10)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 80 L 3 80 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 10)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 33)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 53)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 65.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(4.1000000000000005 78)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(36 10)">
|
||
<path class="typst-shape" fill="#eeeeee" fill-rule="nonzero" d="M 0 0 L 0 79.5 L 3 79.5 L 3 0 Z "/>
|
||
</g>
|
||
<g transform="translate(29.099999999999998 10)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(29.099999999999998 40)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(29.099999999999998 52.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(29.099999999999998 65)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(29.099999999999998 77.5)">
|
||
<g class="typst-group">
|
||
<g>
|
||
<g transform="translate(0 0)">
|
||
<path class="typst-shape" fill="#e6e6e6" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
<g transform="translate(0 30)">
|
||
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 2 L 50 2 L 50 0 Z "/>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="numeral">10</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="instructions">
|
||
<p><em>Interactive flashcards for digital learning. Use the left/right arrow keys or click the cards to flip them.</em></p>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<script>
|
||
// Modal Manager - Handles modal dialogs and fullscreen functionality
|
||
class ModalManager {
|
||
constructor() {
|
||
this.isFullscreen = false;
|
||
this.bindEvents();
|
||
}
|
||
|
||
bindEvents() {
|
||
// Challenge button events
|
||
document.getElementById('open-quiz-modal').addEventListener('click', () => {
|
||
this.openModal('quiz-modal');
|
||
});
|
||
|
||
document.getElementById('open-sorting-modal').addEventListener('click', () => {
|
||
this.openModal('sorting-modal');
|
||
});
|
||
|
||
document.getElementById('open-matching-modal').addEventListener('click', () => {
|
||
this.openModal('matching-modal');
|
||
});
|
||
|
||
// Close button events
|
||
document.getElementById('close-quiz-modal').addEventListener('click', () => {
|
||
this.closeModal('quiz-modal');
|
||
});
|
||
|
||
document.getElementById('close-sorting-modal').addEventListener('click', () => {
|
||
this.closeModal('sorting-modal');
|
||
});
|
||
|
||
document.getElementById('close-matching-modal').addEventListener('click', () => {
|
||
this.closeModal('matching-modal');
|
||
});
|
||
|
||
// Fullscreen button events
|
||
document.getElementById('quiz-fullscreen-btn').addEventListener('click', () => {
|
||
this.toggleFullscreen('quiz-modal');
|
||
});
|
||
|
||
document.getElementById('sorting-fullscreen-btn').addEventListener('click', () => {
|
||
this.toggleFullscreen('sorting-modal');
|
||
});
|
||
|
||
document.getElementById('matching-fullscreen-btn').addEventListener('click', () => {
|
||
this.toggleFullscreen('matching-modal');
|
||
});
|
||
|
||
// Close modal when clicking outside
|
||
document.addEventListener('click', (e) => {
|
||
if (e.target.classList.contains('modal') && e.target.classList.contains('show')) {
|
||
this.closeModal(e.target.id);
|
||
}
|
||
});
|
||
|
||
// ESC key to close modal
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
const openModal = document.querySelector('.modal.show');
|
||
if (openModal) {
|
||
this.closeModal(openModal.id);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Fullscreen change events
|
||
document.addEventListener('fullscreenchange', () => {
|
||
this.handleFullscreenChange();
|
||
});
|
||
|
||
document.addEventListener('webkitfullscreenchange', () => {
|
||
this.handleFullscreenChange();
|
||
});
|
||
|
||
document.addEventListener('mozfullscreenchange', () => {
|
||
this.handleFullscreenChange();
|
||
});
|
||
|
||
document.addEventListener('MSFullscreenChange', () => {
|
||
this.handleFullscreenChange();
|
||
});
|
||
}
|
||
|
||
openModal(modalId) {
|
||
const modal = document.getElementById(modalId);
|
||
modal.classList.add('show');
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
closeModal(modalId) {
|
||
const modal = document.getElementById(modalId);
|
||
if (this.isFullscreen) {
|
||
this.exitFullscreen();
|
||
}
|
||
modal.classList.remove('show', 'fullscreen');
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
async toggleFullscreen(modalId) {
|
||
const modal = document.getElementById(modalId);
|
||
|
||
if (!this.isFullscreen) {
|
||
try {
|
||
if (modal.requestFullscreen) {
|
||
await modal.requestFullscreen();
|
||
} else if (modal.webkitRequestFullscreen) {
|
||
await modal.webkitRequestFullscreen();
|
||
} else if (modal.mozRequestFullScreen) {
|
||
await modal.mozRequestFullScreen();
|
||
} else if (modal.msRequestFullscreen) {
|
||
await modal.msRequestFullscreen();
|
||
}
|
||
modal.classList.add('fullscreen');
|
||
} catch (error) {
|
||
console.warn('Fullscreen not supported or failed:', error);
|
||
// Fallback to CSS fullscreen
|
||
modal.classList.add('fullscreen');
|
||
}
|
||
} else {
|
||
this.exitFullscreen();
|
||
}
|
||
}
|
||
|
||
exitFullscreen() {
|
||
if (document.exitFullscreen) {
|
||
document.exitFullscreen();
|
||
} else if (document.webkitExitFullscreen) {
|
||
document.webkitExitFullscreen();
|
||
} else if (document.mozCancelFullScreen) {
|
||
document.mozCancelFullScreen();
|
||
} else if (document.msExitFullscreen) {
|
||
document.msExitFullscreen();
|
||
}
|
||
}
|
||
|
||
handleFullscreenChange() {
|
||
const isFullscreen = !!(document.fullscreenElement ||
|
||
document.webkitFullscreenElement ||
|
||
document.mozFullScreenElement ||
|
||
document.msFullscreenElement);
|
||
|
||
this.isFullscreen = isFullscreen;
|
||
|
||
if (!isFullscreen) {
|
||
// Remove fullscreen class from all modals when exiting fullscreen
|
||
document.querySelectorAll('.modal').forEach(modal => {
|
||
modal.classList.remove('fullscreen');
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Quiz functionality - No dependencies, pure JavaScript
|
||
class SorobanQuiz {
|
||
constructor() {
|
||
this.cards = [];
|
||
this.quizCards = [];
|
||
this.currentCardIndex = 0;
|
||
this.displayTime = 2.0;
|
||
this.selectedCount = 15;
|
||
this.foundNumbers = [];
|
||
this.correctAnswers = [];
|
||
this.guessesRemaining = 0;
|
||
this.currentInput = '';
|
||
this.incorrectGuesses = 0;
|
||
this.finishButtonsBound = false;
|
||
|
||
this.initializeCards();
|
||
this.bindEvents();
|
||
}
|
||
|
||
initializeCards() {
|
||
// Extract card data from the DOM
|
||
const cardElements = document.querySelectorAll('.flashcard');
|
||
this.cards = Array.from(cardElements).map(card => ({
|
||
number: parseInt(card.dataset.number),
|
||
svg: card.querySelector('.abacus-container').innerHTML,
|
||
element: card
|
||
}));
|
||
}
|
||
|
||
bindEvents() {
|
||
// Count buttons
|
||
document.querySelectorAll('.count-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
document.querySelectorAll('.count-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
this.selectedCount = btn.dataset.count === 'all' ? this.cards.length : parseInt(btn.dataset.count);
|
||
});
|
||
});
|
||
|
||
// Display time slider
|
||
const slider = document.getElementById('display-time');
|
||
const valueDisplay = document.querySelector('.slider-value');
|
||
slider.addEventListener('input', (e) => {
|
||
this.displayTime = parseFloat(e.target.value);
|
||
valueDisplay.textContent = this.displayTime.toFixed(1) + 's';
|
||
});
|
||
|
||
// Start quiz button
|
||
document.getElementById('start-quiz').addEventListener('click', () => {
|
||
this.startQuiz();
|
||
});
|
||
|
||
// Note: Submit answers button replaced by smart input system
|
||
|
||
// Note: Finish quiz buttons are bound later in showInputPhase() when they become visible
|
||
|
||
// End quiz button
|
||
document.getElementById('end-quiz').addEventListener('click', () => {
|
||
this.endQuiz();
|
||
});
|
||
|
||
// Retry and back buttons
|
||
document.getElementById('retry-quiz').addEventListener('click', () => {
|
||
this.resetQuiz();
|
||
this.startQuiz();
|
||
});
|
||
|
||
document.getElementById('back-to-cards').addEventListener('click', () => {
|
||
this.resetQuiz();
|
||
});
|
||
}
|
||
|
||
async startQuiz() {
|
||
// Select random cards
|
||
this.quizCards = this.getRandomCards(this.selectedCount);
|
||
this.correctAnswers = this.quizCards.map(card => card.number);
|
||
this.currentCardIndex = 0;
|
||
|
||
// Hide configuration controls, show only game
|
||
document.querySelector('.quiz-controls').style.display = 'none';
|
||
|
||
// Show quiz game section within modal
|
||
this.hideQuizSections();
|
||
document.getElementById('quiz-game').style.display = 'block';
|
||
document.getElementById('total-cards').textContent = this.quizCards.length;
|
||
|
||
// Start with the first card
|
||
this.showNextCard();
|
||
}
|
||
|
||
getRandomCards(count) {
|
||
const shuffled = [...this.cards].sort(() => 0.5 - Math.random());
|
||
return shuffled.slice(0, Math.min(count, this.cards.length));
|
||
}
|
||
|
||
async showNextCard() {
|
||
if (this.currentCardIndex >= this.quizCards.length) {
|
||
this.showInputPhase();
|
||
return;
|
||
}
|
||
|
||
const card = this.quizCards[this.currentCardIndex];
|
||
const progress = ((this.currentCardIndex) / this.quizCards.length) * 100;
|
||
|
||
// Update progress
|
||
document.querySelector('.progress-fill').style.width = progress + '%';
|
||
document.getElementById('current-card').textContent = this.currentCardIndex + 1;
|
||
|
||
// Only show countdown for the very first card
|
||
if (this.currentCardIndex === 0) {
|
||
await this.showCountdown();
|
||
} else {
|
||
// Subtle "new card" indicator for subsequent cards
|
||
await this.showNewCardIndicator();
|
||
}
|
||
|
||
// Show card
|
||
await this.displayCard(card);
|
||
|
||
this.currentCardIndex++;
|
||
|
||
// Minimal delay before next card (just enough for the exit animation)
|
||
setTimeout(() => {
|
||
this.showNextCard();
|
||
}, 100);
|
||
}
|
||
|
||
async showNewCardIndicator() {
|
||
return new Promise(resolve => {
|
||
const countdownEl = document.getElementById('quiz-countdown');
|
||
const cardEl = document.getElementById('quiz-card');
|
||
|
||
// Hide card temporarily
|
||
cardEl.style.display = 'none';
|
||
countdownEl.style.display = 'block';
|
||
|
||
// Brief flash to indicate new card
|
||
countdownEl.textContent = 'Next';
|
||
countdownEl.className = 'quiz-countdown new-card-flash';
|
||
|
||
setTimeout(() => {
|
||
countdownEl.style.display = 'none';
|
||
resolve();
|
||
}, 150); // Very brief indication
|
||
});
|
||
}
|
||
|
||
async showCountdown() {
|
||
const countdownEl = document.getElementById('quiz-countdown');
|
||
const cardEl = document.getElementById('quiz-card');
|
||
|
||
cardEl.style.visibility = 'hidden';
|
||
countdownEl.style.display = 'block';
|
||
|
||
// 3, 2, 1 countdown
|
||
const counts = ['3', '2', '1', 'GO!'];
|
||
|
||
for (let i = 0; i < counts.length; i++) {
|
||
countdownEl.textContent = counts[i];
|
||
countdownEl.className = 'countdown';
|
||
if (i === counts.length - 1) countdownEl.classList.add('go');
|
||
|
||
await this.delay(400);
|
||
}
|
||
|
||
countdownEl.style.display = 'none';
|
||
}
|
||
|
||
async displayCard(card) {
|
||
const cardEl = document.getElementById('quiz-card');
|
||
const countdownEl = document.getElementById('quiz-countdown');
|
||
|
||
// Show card content with entry animation
|
||
cardEl.innerHTML = card.svg;
|
||
cardEl.style.display = 'block';
|
||
cardEl.style.visibility = 'visible';
|
||
cardEl.classList.add('pulse');
|
||
|
||
// Display for most of the time
|
||
await this.delay((this.displayTime * 1000) - 300);
|
||
|
||
// Subtle exit signal - brief red border flash
|
||
cardEl.classList.add('card-exit-warning');
|
||
await this.delay(200);
|
||
|
||
// Quick fade out
|
||
cardEl.classList.add('card-fade-out');
|
||
await this.delay(100);
|
||
|
||
// Hide card and reset classes
|
||
cardEl.classList.remove('pulse', 'card-exit-warning', 'card-fade-out');
|
||
cardEl.style.visibility = 'hidden';
|
||
}
|
||
|
||
showInputPhase() {
|
||
// Complete progress bar
|
||
document.querySelector('.progress-fill').style.width = '100%';
|
||
|
||
// Initialize smart input system
|
||
this.correctAnswers = this.quizCards.map(card => card.number);
|
||
this.foundNumbers = [];
|
||
this.guessesRemaining = this.selectedCount + Math.floor(this.selectedCount / 2); // Allow 50% extra guesses
|
||
|
||
// Update stats display
|
||
document.getElementById('cards-shown-count').textContent = this.quizCards.length;
|
||
document.getElementById('guesses-remaining').textContent = this.guessesRemaining;
|
||
document.getElementById('numbers-found').textContent = '0';
|
||
|
||
// Hide quiz game, show input
|
||
this.hideQuizSections();
|
||
document.getElementById('quiz-input').style.display = 'block';
|
||
|
||
// Setup smart input
|
||
const smartInput = document.getElementById('smart-input');
|
||
const display = document.getElementById('number-display');
|
||
smartInput.value = '';
|
||
document.getElementById('current-typing').textContent = '';
|
||
|
||
// Focus the hidden input and make sure it captures keyboard events
|
||
smartInput.focus();
|
||
|
||
// Remove any existing event listeners to prevent duplicates
|
||
const newSmartInput = smartInput.cloneNode(true);
|
||
smartInput.parentNode.replaceChild(newSmartInput, smartInput);
|
||
|
||
// Add input event listener for real-time validation
|
||
newSmartInput.addEventListener('input', (e) => this.handleSmartInput(e));
|
||
|
||
// Make the display area clickable to maintain focus
|
||
display.addEventListener('click', () => {
|
||
newSmartInput.focus();
|
||
});
|
||
|
||
// Keep focus on the hidden input
|
||
newSmartInput.focus();
|
||
|
||
// Bind finish buttons (they exist now that quiz-input is shown)
|
||
this.bindFinishButtons();
|
||
|
||
// Show finish button when all numbers found or guesses exhausted
|
||
this.updateFinishButtonVisibility();
|
||
}
|
||
|
||
bindFinishButtons() {
|
||
// Bind finish quiz buttons - called when input phase starts and buttons are visible
|
||
// Only bind once to prevent duplicate listeners
|
||
if (this.finishButtonsBound) return;
|
||
|
||
const finishBtn = document.getElementById('finish-quiz');
|
||
const giveUpBtn = document.getElementById('give-up-quiz');
|
||
|
||
if (finishBtn) {
|
||
finishBtn.addEventListener('click', () => {
|
||
console.log('Finish quiz button clicked');
|
||
this.finishQuiz();
|
||
});
|
||
console.log('Finish button event listener added');
|
||
} else {
|
||
console.error('finish-quiz button not found in DOM');
|
||
}
|
||
|
||
if (giveUpBtn) {
|
||
giveUpBtn.addEventListener('click', () => {
|
||
console.log('Give up button clicked');
|
||
this.finishQuiz();
|
||
});
|
||
console.log('Give up button event listener added');
|
||
} else {
|
||
console.error('give-up-quiz button not found in DOM');
|
||
}
|
||
|
||
this.finishButtonsBound = true;
|
||
}
|
||
|
||
handleSmartInput(event) {
|
||
const input = event.target;
|
||
const value = input.value.trim();
|
||
const display = document.getElementById('number-display');
|
||
const typingSpan = document.getElementById('current-typing');
|
||
|
||
// Reset visual feedback
|
||
display.classList.remove('correct', 'incorrect');
|
||
|
||
// Update the visual display
|
||
typingSpan.textContent = value;
|
||
|
||
// Check if input is empty
|
||
if (!value) {
|
||
this.currentInput = '';
|
||
return;
|
||
}
|
||
|
||
// Check if it's a valid number
|
||
const number = parseInt(value);
|
||
if (isNaN(number)) {
|
||
return; // Wait for more input
|
||
}
|
||
|
||
this.currentInput = value;
|
||
|
||
// Check if this number is in our correct answers and not already found
|
||
if (this.correctAnswers.includes(number) && !this.foundNumbers.includes(number)) {
|
||
// Correct number found!
|
||
this.acceptCorrectNumber(number, input, display);
|
||
} else if (value.length >= 2 && !this.correctAnswers.includes(number)) {
|
||
// Wrong number (only trigger after at least 2 digits to avoid false positives)
|
||
this.handleIncorrectGuess(input, display);
|
||
}
|
||
}
|
||
|
||
acceptCorrectNumber(number, input, display) {
|
||
// Add to found numbers
|
||
this.foundNumbers.push(number);
|
||
|
||
// Visual success feedback
|
||
display.classList.add('correct');
|
||
|
||
// Update stats
|
||
document.getElementById('numbers-found').textContent = this.foundNumbers.length;
|
||
|
||
// Add to found numbers display
|
||
this.addFoundNumberDisplay(number);
|
||
|
||
// Clear input immediately for fast entry
|
||
setTimeout(() => {
|
||
input.value = '';
|
||
document.getElementById('current-typing').textContent = '';
|
||
this.currentInput = '';
|
||
|
||
// Check if we're done
|
||
this.updateFinishButtonVisibility();
|
||
|
||
// If all numbers found, auto-finish
|
||
if (this.foundNumbers.length === this.correctAnswers.length) {
|
||
setTimeout(() => this.finishQuiz(), 1000);
|
||
}
|
||
}, 150); // Much shorter delay - just enough to show the success feedback
|
||
|
||
// Remove success visual feedback after animation completes
|
||
setTimeout(() => {
|
||
display.classList.remove('correct');
|
||
}, 500);
|
||
}
|
||
|
||
handleIncorrectGuess(input, display) {
|
||
// Only penalize if we have guesses remaining
|
||
if (this.guessesRemaining > 0) {
|
||
this.guessesRemaining--;
|
||
this.incorrectGuesses++; // Track incorrect guesses for scoring
|
||
document.getElementById('guesses-remaining').textContent = this.guessesRemaining;
|
||
|
||
// Visual error feedback
|
||
display.classList.add('incorrect');
|
||
|
||
// Clear input quickly for rapid entry
|
||
setTimeout(() => {
|
||
input.value = '';
|
||
document.getElementById('current-typing').textContent = '';
|
||
this.currentInput = '';
|
||
|
||
// Check if we're out of guesses
|
||
this.updateFinishButtonVisibility();
|
||
if (this.guessesRemaining === 0) {
|
||
setTimeout(() => this.finishQuiz(), 1000);
|
||
}
|
||
}, 150); // Same fast clearing as correct numbers
|
||
|
||
// Remove error visual feedback after animation completes
|
||
setTimeout(() => {
|
||
display.classList.remove('incorrect');
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
addFoundNumberDisplay(number) {
|
||
const foundContainer = document.getElementById('found-numbers');
|
||
const numberElement = document.createElement('span');
|
||
numberElement.className = 'found-number';
|
||
numberElement.textContent = number;
|
||
foundContainer.appendChild(numberElement);
|
||
}
|
||
|
||
updateFinishButtonVisibility() {
|
||
const finishBtn = document.getElementById('finish-quiz');
|
||
const giveUpBtn = document.getElementById('give-up-quiz');
|
||
|
||
const hasFoundSome = this.foundNumbers.length > 0;
|
||
const hasFoundAll = this.foundNumbers.length === this.correctAnswers.length;
|
||
const outOfGuesses = this.guessesRemaining === 0;
|
||
const hasGuessesLeft = this.guessesRemaining > 0;
|
||
|
||
if (hasFoundAll || outOfGuesses) {
|
||
// Show finish button when all found or no guesses left
|
||
finishBtn.style.display = 'block';
|
||
giveUpBtn.style.display = 'none';
|
||
finishBtn.textContent = hasFoundAll ? 'Finish Quiz' : 'Show Results';
|
||
} else if (hasFoundSome && hasGuessesLeft) {
|
||
// Show both buttons when user has found some but could find more
|
||
finishBtn.style.display = 'block';
|
||
giveUpBtn.style.display = 'block';
|
||
finishBtn.textContent = 'Show Results';
|
||
} else {
|
||
// No buttons when user hasn't found any yet
|
||
finishBtn.style.display = 'none';
|
||
giveUpBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
finishQuiz() {
|
||
console.log('finishQuiz called, foundNumbers:', this.foundNumbers);
|
||
// Use found numbers as answers and show results
|
||
this.answers = [...this.foundNumbers];
|
||
console.log('About to call showResults with answers:', this.answers);
|
||
this.showResults();
|
||
}
|
||
|
||
submitAnswers() {
|
||
const input = document.getElementById('answer-input').value;
|
||
this.answers = this.parseAnswers(input);
|
||
this.showResults();
|
||
}
|
||
|
||
parseAnswers(input) {
|
||
// Parse comma or space separated numbers
|
||
return input.split(/[,\s]+/)
|
||
.map(s => s.trim())
|
||
.filter(s => s.length > 0)
|
||
.map(s => parseInt(s))
|
||
.filter(n => !isNaN(n));
|
||
}
|
||
|
||
showResults() {
|
||
const scoreData = this.calculateScore();
|
||
const correct = scoreData.correct;
|
||
const finalScore = scoreData.finalScore;
|
||
const percentage = Math.round(finalScore);
|
||
|
||
// Update score display
|
||
document.getElementById('score-percentage').textContent = percentage + '%';
|
||
document.getElementById('score-correct').textContent = correct.length;
|
||
document.getElementById('score-total').textContent = this.correctAnswers.length;
|
||
document.getElementById('result-timing').textContent = this.displayTime.toFixed(1) + 's';
|
||
|
||
// Show detailed results with penalty info
|
||
this.showDetailedResults(correct, scoreData);
|
||
|
||
// Hide input, show results
|
||
this.hideQuizSections();
|
||
document.getElementById('quiz-results').style.display = 'block';
|
||
}
|
||
|
||
calculateScore() {
|
||
const correct = [];
|
||
const correctSet = new Set(this.correctAnswers);
|
||
const answerSet = new Set(this.answers);
|
||
|
||
// Find correct answers
|
||
this.answers.forEach(answer => {
|
||
if (correctSet.has(answer)) {
|
||
correct.push(answer);
|
||
}
|
||
});
|
||
|
||
// Calculate base score as percentage of correct answers
|
||
const baseScore = (correct.length / this.correctAnswers.length) * 100;
|
||
|
||
// Calculate penalty: lose 5 points per incorrect guess, minimum 0%
|
||
const penalty = this.incorrectGuesses * 5;
|
||
const finalScore = Math.max(0, baseScore - penalty);
|
||
|
||
return {
|
||
correct: correct,
|
||
baseScore: baseScore,
|
||
penalty: penalty,
|
||
incorrectGuesses: this.incorrectGuesses,
|
||
finalScore: finalScore
|
||
};
|
||
}
|
||
|
||
showDetailedResults(correct, scoreData) {
|
||
const resultsEl = document.getElementById('results-list');
|
||
const correctSet = new Set(correct);
|
||
const answerSet = new Set(this.answers);
|
||
|
||
let html = '';
|
||
|
||
// Show scoring breakdown if there were penalties
|
||
if (scoreData && scoreData.incorrectGuesses > 0) {
|
||
html += `<div class="result-item score-breakdown">
|
||
<div style="margin-bottom: 10px; font-weight: bold; color: #2c5f76;">Score Breakdown:</div>
|
||
<div style="font-size: 0.9em; color: #666;">
|
||
Base Score: ${Math.round(scoreData.baseScore)}% (${correct.length} of ${this.correctAnswers.length} correct)<br>
|
||
Penalty: -${scoreData.penalty}% (${scoreData.incorrectGuesses} wrong guess${scoreData.incorrectGuesses > 1 ? 'es' : ''} × 5 points each)<br>
|
||
<strong style="color: #2c5f76;">Final Score: ${Math.round(scoreData.finalScore)}%</strong>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// Show all correct answers and whether user got them
|
||
this.correctAnswers.forEach(num => {
|
||
const wasCorrect = correctSet.has(num);
|
||
const className = wasCorrect ? 'result-correct' : 'result-incorrect';
|
||
const status = wasCorrect ? '✓ Correct' : '✗ Missed';
|
||
html += `<div class="result-item">
|
||
<span>Card: ${num}</span>
|
||
<span class="${className}">${status}</span>
|
||
</div>`;
|
||
});
|
||
|
||
// Show any extra incorrect answers
|
||
const extraAnswers = this.answers.filter(a => !correctSet.has(a) && this.correctAnswers.includes(a) === false);
|
||
extraAnswers.forEach(num => {
|
||
html += `<div class="result-item">
|
||
<span>Wrong guess: ${num}</span>
|
||
<span class="result-incorrect">✗ Not in quiz (-5 points)</span>
|
||
</div>`;
|
||
});
|
||
|
||
resultsEl.innerHTML = html;
|
||
}
|
||
|
||
endQuiz() {
|
||
// Stop the current quiz and return to configuration
|
||
this.resetQuiz();
|
||
}
|
||
|
||
resetQuiz() {
|
||
// Reset state
|
||
this.currentCardIndex = 0;
|
||
this.answers = [];
|
||
this.correctAnswers = [];
|
||
this.quizCards = [];
|
||
this.foundNumbers = [];
|
||
this.guessesRemaining = 0;
|
||
this.currentInput = '';
|
||
this.incorrectGuesses = 0;
|
||
this.finishButtonsBound = false;
|
||
|
||
// Clear smart input
|
||
const smartInput = document.getElementById('smart-input');
|
||
if (smartInput) {
|
||
smartInput.value = '';
|
||
smartInput.classList.remove('correct', 'incorrect');
|
||
}
|
||
|
||
// Clear found numbers display
|
||
const foundContainer = document.getElementById('found-numbers');
|
||
if (foundContainer) {
|
||
foundContainer.innerHTML = '';
|
||
}
|
||
|
||
// Reset stats display
|
||
document.getElementById('cards-shown-count').textContent = '0';
|
||
document.getElementById('guesses-remaining').textContent = '0';
|
||
document.getElementById('numbers-found').textContent = '0';
|
||
|
||
// Hide finish buttons
|
||
document.getElementById('finish-quiz').style.display = 'none';
|
||
document.getElementById('give-up-quiz').style.display = 'none';
|
||
|
||
// Reset to initial quiz state (hide all sections, show controls)
|
||
this.hideQuizSections();
|
||
document.querySelector('.quiz-controls').style.display = 'block';
|
||
}
|
||
|
||
hideQuizSections() {
|
||
document.getElementById('quiz-game').style.display = 'none';
|
||
document.getElementById('quiz-input').style.display = 'none';
|
||
document.getElementById('quiz-results').style.display = 'none';
|
||
}
|
||
|
||
delay(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
}
|
||
|
||
// Card Sorting Challenge - Drag and drop functionality
|
||
class SortingChallenge {
|
||
constructor() {
|
||
this.cards = [];
|
||
this.sortingCards = [];
|
||
this.selectedCount = 5;
|
||
this.currentOrder = [];
|
||
this.correctOrder = [];
|
||
this.isDragging = false;
|
||
this.draggedElement = null;
|
||
this.draggedIndex = -1;
|
||
this.previewOrder = [];
|
||
this.lastInsertIndex = -1;
|
||
this.touchStartElement = null;
|
||
this.touchStartIndex = -1;
|
||
this.touchStartX = 0;
|
||
this.touchStartY = 0;
|
||
|
||
this.initializeSorting();
|
||
this.bindSortingEvents();
|
||
}
|
||
|
||
initializeSorting() {
|
||
// Get available cards (same as quiz cards)
|
||
const cardElements = document.querySelectorAll('.flashcard');
|
||
this.cards = Array.from(cardElements).map(card => ({
|
||
number: parseInt(card.dataset.number),
|
||
svg: card.querySelector('.abacus-container').outerHTML
|
||
}));
|
||
}
|
||
|
||
bindSortingEvents() {
|
||
// Card count selection
|
||
document.querySelectorAll('.sort-count-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
document.querySelectorAll('.sort-count-btn').forEach(b => b.classList.remove('active'));
|
||
e.target.classList.add('active');
|
||
this.selectedCount = parseInt(e.target.dataset.count);
|
||
});
|
||
});
|
||
|
||
// Action buttons
|
||
document.getElementById('start-sorting').addEventListener('click', () => this.startSorting());
|
||
document.getElementById('check-sorting').addEventListener('click', () => this.checkSolution());
|
||
document.getElementById('reveal-numbers').addEventListener('click', () => this.revealNumbers());
|
||
document.getElementById('new-sorting').addEventListener('click', () => this.newChallenge());
|
||
document.getElementById('end-sorting').addEventListener('click', () => this.endSorting());
|
||
|
||
// Score modal event listeners
|
||
document.querySelector('.score-modal-close').addEventListener('click', () => this.hideScoreModal());
|
||
document.getElementById('score-modal-close-btn').addEventListener('click', () => this.hideScoreModal());
|
||
document.getElementById('score-modal-new-game').addEventListener('click', () => {
|
||
this.hideScoreModal();
|
||
this.newChallenge();
|
||
});
|
||
|
||
// Close modal when clicking outside
|
||
document.getElementById('score-modal').addEventListener('click', (e) => {
|
||
if (e.target.id === 'score-modal') {
|
||
this.hideScoreModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
startSorting() {
|
||
// Select random cards for sorting
|
||
const shuffledCards = [...this.cards].sort(() => Math.random() - 0.5);
|
||
this.sortingCards = shuffledCards.slice(0, this.selectedCount);
|
||
this.correctOrder = [...this.sortingCards].sort((a, b) => a.number - b.number);
|
||
|
||
// Shuffle the display order
|
||
this.currentOrder = [...this.sortingCards].sort(() => Math.random() - 0.5);
|
||
|
||
// Initialize revealed state
|
||
this.numbersRevealed = false;
|
||
|
||
// Hide configuration controls, show only game
|
||
document.querySelector('.sorting-controls').style.display = 'none';
|
||
|
||
// Show sorting game and sticky header
|
||
document.getElementById('sorting-game').style.display = 'block';
|
||
document.getElementById('sorting-header').style.display = 'block';
|
||
|
||
this.renderSortingCards();
|
||
this.updateSortingStatus(`Arrange the ${this.selectedCount} cards in ascending order (smallest to largest)`);
|
||
|
||
// Start timer
|
||
this.startTimer();
|
||
|
||
// Reset reveal numbers button for new game
|
||
document.getElementById('reveal-numbers').style.display = 'inline-block';
|
||
|
||
// Update buttons - hide old controls, sticky header is now visible
|
||
document.getElementById('start-sorting').style.display = 'none';
|
||
document.querySelector('.sorting-game-actions').style.display = 'none';
|
||
}
|
||
|
||
getSequenceStyle(position, totalSlots) {
|
||
// Generate neutral warm gray gradient that won't clash with abacus colors
|
||
// Position 0 (first) = darkest, last position = lightest
|
||
const intensity = position / (totalSlots - 1); // 0 to 1
|
||
const lightness = 30 + (intensity * 45); // 30% to 75% lightness for better contrast
|
||
return {
|
||
background: `hsl(220, 8%, ${lightness}%)`, // Very subtle blue-gray, low saturation
|
||
color: lightness > 60 ? '#2c3e50' : '#ffffff', // High contrast text
|
||
borderColor: lightness > 60 ? '#2c5f76' : 'rgba(255,255,255,0.4)'
|
||
};
|
||
}
|
||
|
||
renderSortingCards() {
|
||
this.createPositionSlots();
|
||
this.renderAvailableCards();
|
||
}
|
||
|
||
createPositionSlots() {
|
||
const slotsContainer = document.getElementById('position-slots');
|
||
if (!slotsContainer) return;
|
||
|
||
slotsContainer.innerHTML = '';
|
||
this.placedCards = new Array(this.selectedCount).fill(null);
|
||
|
||
// Add insert button before the very first position
|
||
const firstInsertBtn = document.createElement('button');
|
||
firstInsertBtn.className = 'insert-button';
|
||
firstInsertBtn.innerHTML = '+';
|
||
firstInsertBtn.dataset.insertAt = 0;
|
||
firstInsertBtn.addEventListener('click', (e) => this.handleInsertClick(0));
|
||
slotsContainer.appendChild(firstInsertBtn);
|
||
|
||
for (let i = 0; i < this.selectedCount; i++) {
|
||
// Add the position slot
|
||
const slot = document.createElement('div');
|
||
slot.className = 'position-slot';
|
||
slot.dataset.position = i;
|
||
|
||
// Apply gradient to entire slot background
|
||
const style = this.getSequenceStyle(i, this.selectedCount);
|
||
slot.style.background = style.background;
|
||
slot.style.color = style.color;
|
||
slot.style.borderColor = style.borderColor;
|
||
|
||
slot.innerHTML = `
|
||
<div class="slot-label" style="color: ${style.color}">${i === 0 ? 'Smallest' : i === this.selectedCount - 1 ? 'Largest' : ''}</div>
|
||
`;
|
||
|
||
slot.addEventListener('click', (e) => this.handleSlotClick(i));
|
||
slotsContainer.appendChild(slot);
|
||
|
||
// Add insert button after each position
|
||
const insertBtn = document.createElement('button');
|
||
insertBtn.className = 'insert-button';
|
||
insertBtn.innerHTML = '+';
|
||
insertBtn.dataset.insertAt = i + 1;
|
||
insertBtn.addEventListener('click', (e) => this.handleInsertClick(i + 1));
|
||
slotsContainer.appendChild(insertBtn);
|
||
}
|
||
}
|
||
|
||
renderAvailableCards() {
|
||
const sortingArea = document.getElementById('sorting-area');
|
||
if (!sortingArea) return;
|
||
|
||
sortingArea.innerHTML = '';
|
||
|
||
// Remove duplicates and filter out placed cards
|
||
const uniqueAvailable = this.currentOrder.filter((card, index, arr) => {
|
||
// Only keep first occurrence of each card number
|
||
const firstIndex = arr.findIndex(c => c.number === card.number);
|
||
if (firstIndex !== index) {
|
||
console.warn(`Duplicate card found: ${card.number}, removing duplicate`);
|
||
return false;
|
||
}
|
||
|
||
// Skip if already placed
|
||
if (this.placedCards.some(placed => placed && placed.number === card.number)) {
|
||
console.warn(`Card ${card.number} is both available and placed, removing from available`);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
// Update currentOrder to clean version
|
||
this.currentOrder = uniqueAvailable;
|
||
|
||
this.currentOrder.forEach((card, index) => {
|
||
const cardEl = document.createElement('div');
|
||
cardEl.className = 'sort-card';
|
||
cardEl.dataset.number = card.number;
|
||
|
||
// Apply revealed state if numbers were previously revealed
|
||
if (this.numbersRevealed) {
|
||
cardEl.classList.add('revealed');
|
||
}
|
||
|
||
cardEl.innerHTML = `
|
||
<div class="revealed-number">${card.number}</div>
|
||
<div class="card-svg">${card.svg}</div>
|
||
`;
|
||
|
||
cardEl.addEventListener('click', (e) => this.handleCardClick(card, cardEl));
|
||
sortingArea.appendChild(cardEl);
|
||
});
|
||
}
|
||
|
||
handleCardClick(card, cardElement) {
|
||
// Clear any previously selected cards
|
||
document.querySelectorAll('.sort-card.selected').forEach(el => {
|
||
el.classList.remove('selected');
|
||
});
|
||
|
||
// Select this card
|
||
cardElement.classList.add('selected');
|
||
this.selectedCard = card;
|
||
this.selectedCardElement = cardElement;
|
||
|
||
// Highlight available positions and insert buttons
|
||
document.querySelectorAll('.position-slot').forEach(slot => {
|
||
if (!slot.classList.contains('filled')) {
|
||
slot.classList.add('active');
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll('.insert-button').forEach(btn => {
|
||
btn.classList.add('active');
|
||
});
|
||
|
||
this.updateSortingStatus(`Selected card with value ${card.number}. Click a position or + button to place it.`);
|
||
}
|
||
|
||
handleInsertClick(insertPosition) {
|
||
if (!this.selectedCard) {
|
||
this.updateSortingStatus('Please select a card first, then click where to insert it.');
|
||
return;
|
||
}
|
||
|
||
// Handle insertion at the rightmost position (beyond current array bounds)
|
||
if (insertPosition >= this.selectedCount) {
|
||
// Find the rightmost empty position
|
||
let rightmostEmptyPos = -1;
|
||
for (let i = this.selectedCount - 1; i >= 0; i--) {
|
||
if (this.placedCards[i] === null) {
|
||
rightmostEmptyPos = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (rightmostEmptyPos === -1) {
|
||
this.updateSortingStatus('All positions are filled. Move a card first to make room.');
|
||
return;
|
||
}
|
||
|
||
// Place card in the rightmost empty position
|
||
this.placedCards[rightmostEmptyPos] = this.selectedCard;
|
||
} else {
|
||
// Create a new array for placed cards
|
||
const newPlacedCards = new Array(this.selectedCount).fill(null);
|
||
|
||
// Copy existing cards, shifting them as needed
|
||
for (let i = 0; i < this.placedCards.length; i++) {
|
||
if (this.placedCards[i] !== null) {
|
||
if (i < insertPosition) {
|
||
// Cards before insert position stay in same place
|
||
newPlacedCards[i] = this.placedCards[i];
|
||
} else {
|
||
// Cards at or after insert position shift right by 1
|
||
if (i + 1 < this.selectedCount) {
|
||
newPlacedCards[i + 1] = this.placedCards[i];
|
||
} else {
|
||
// Card would fall off - store temporarily, will handle below
|
||
newPlacedCards[i + 1] = this.placedCards[i];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Place the selected card at the insert position
|
||
newPlacedCards[insertPosition] = this.selectedCard;
|
||
|
||
// Now apply gap-filling logic: shift cards left to compress and eliminate gaps
|
||
const compactedCards = [];
|
||
for (let i = 0; i < newPlacedCards.length; i++) {
|
||
if (newPlacedCards[i] !== null) {
|
||
compactedCards.push(newPlacedCards[i]);
|
||
}
|
||
}
|
||
|
||
// If we have more cards than positions, put excess back in available
|
||
if (compactedCards.length > this.selectedCount) {
|
||
const excessCards = compactedCards.slice(this.selectedCount);
|
||
this.currentOrder.push(...excessCards);
|
||
compactedCards.splice(this.selectedCount);
|
||
}
|
||
|
||
// Fill the final array with compacted cards (no gaps)
|
||
const finalPlacedCards = new Array(this.selectedCount).fill(null);
|
||
for (let i = 0; i < compactedCards.length; i++) {
|
||
finalPlacedCards[i] = compactedCards[i];
|
||
}
|
||
|
||
this.placedCards = finalPlacedCards;
|
||
}
|
||
|
||
// Remove card from available cards
|
||
this.currentOrder = this.currentOrder.filter(c => c.number !== this.selectedCard.number);
|
||
|
||
// Debug: Check total card count
|
||
this.debugCardCount('after insert');
|
||
|
||
// Clear selection and re-render
|
||
this.clearSelection();
|
||
this.updatePositionSlots();
|
||
this.renderAvailableCards();
|
||
|
||
// Update status
|
||
const placedCount = this.placedCards.filter(c => c !== null).length;
|
||
if (placedCount === this.selectedCount) {
|
||
this.updateSortingStatus('All cards placed! Click "Check My Solution" to see how you did.');
|
||
} else {
|
||
this.updateSortingStatus(`${placedCount}/${this.selectedCount} cards placed. Select another card to continue.`);
|
||
}
|
||
}
|
||
|
||
debugCardCount(context) {
|
||
const placedCount = this.placedCards.filter(c => c !== null).length;
|
||
const availableCount = this.currentOrder.length;
|
||
const totalCount = placedCount + availableCount;
|
||
|
||
console.log(`DEBUG ${context}: Placed=${placedCount}, Available=${availableCount}, Total=${totalCount}, Expected=${this.selectedCount}`);
|
||
|
||
if (totalCount !== this.selectedCount) {
|
||
console.error('Card count mismatch! Some cards are missing or duplicated.');
|
||
console.log('Placed cards:', this.placedCards.map(c => c ? c.number : 'empty'));
|
||
console.log('Available cards:', this.currentOrder.map(c => c.number));
|
||
}
|
||
}
|
||
|
||
updatePositionSlots() {
|
||
this.placedCards.forEach((card, position) => {
|
||
const slot = document.querySelector(`[data-position="${position}"]`);
|
||
if (!slot) return;
|
||
|
||
if (card) {
|
||
slot.classList.add('filled');
|
||
// Reset to white background when filled
|
||
slot.style.background = '#fff';
|
||
slot.style.color = '#333';
|
||
slot.style.borderColor = '#2c5f76';
|
||
slot.innerHTML = `
|
||
<div class="slot-card">
|
||
<div class="card-svg">${card.svg}</div>
|
||
</div>
|
||
<div class="slot-label">← Click to move back</div>
|
||
`;
|
||
} else {
|
||
slot.classList.remove('filled');
|
||
// Apply gradient to empty slot
|
||
const style = this.getSequenceStyle(position, this.selectedCount);
|
||
slot.style.background = style.background;
|
||
slot.style.color = style.color;
|
||
slot.style.borderColor = style.borderColor;
|
||
slot.innerHTML = `
|
||
<div class="slot-label" style="color: ${style.color}">${position === 0 ? 'Smallest' : position === this.selectedCount - 1 ? 'Largest' : ''}</div>
|
||
`;
|
||
}
|
||
});
|
||
}
|
||
|
||
clearSelection() {
|
||
this.selectedCard = null;
|
||
this.selectedCardElement = null;
|
||
|
||
// Remove active states
|
||
document.querySelectorAll('.sort-card.selected').forEach(el => {
|
||
el.classList.remove('selected');
|
||
});
|
||
document.querySelectorAll('.position-slot.active').forEach(el => {
|
||
el.classList.remove('active');
|
||
});
|
||
document.querySelectorAll('.insert-button.active').forEach(el => {
|
||
el.classList.remove('active');
|
||
});
|
||
}
|
||
|
||
handleSlotClick(position) {
|
||
const slot = document.querySelector(`[data-position="${position}"]`);
|
||
|
||
// If no card is selected but slot has a card, move it back to available
|
||
if (!this.selectedCard && this.placedCards[position]) {
|
||
const cardToMove = this.placedCards[position];
|
||
|
||
// Remove card from this position
|
||
this.placedCards[position] = null;
|
||
|
||
// Add card back to available cards
|
||
this.currentOrder.push(cardToMove);
|
||
|
||
// Debug: Check total card count
|
||
this.debugCardCount('after moving card back');
|
||
|
||
// Update slot appearance
|
||
this.updatePositionSlots();
|
||
this.renderAvailableCards();
|
||
|
||
// Auto-select the moved card so user can immediately place it elsewhere
|
||
this.selectedCard = cardToMove;
|
||
this.selectedCardElement = document.querySelector(`[data-number="${cardToMove.number}"]`);
|
||
if (this.selectedCardElement) {
|
||
this.selectedCardElement.classList.add('selected');
|
||
|
||
// Highlight available positions
|
||
document.querySelectorAll('.position-slot').forEach(slot => {
|
||
if (!slot.classList.contains('filled')) {
|
||
slot.classList.add('active');
|
||
}
|
||
});
|
||
document.querySelectorAll('.insert-button').forEach(btn => {
|
||
btn.classList.add('active');
|
||
btn.classList.remove('disabled');
|
||
});
|
||
}
|
||
|
||
const placedCount = this.placedCards.filter(c => c !== null).length;
|
||
this.updateSortingStatus(`Moved card ${cardToMove.number} back and selected it. ${placedCount}/${this.selectedCount} cards placed.`);
|
||
return;
|
||
}
|
||
|
||
if (!this.selectedCard) {
|
||
this.updateSortingStatus('Select a card first, or click a placed card to move it back.');
|
||
return;
|
||
}
|
||
|
||
// If slot is already filled, replace the card
|
||
if (this.placedCards[position]) {
|
||
// Move the previous card back to available area
|
||
this.currentOrder.push(this.placedCards[position]);
|
||
}
|
||
|
||
// Place the selected card in this position
|
||
this.placedCards[position] = this.selectedCard;
|
||
|
||
// Remove card from current order (available cards)
|
||
this.currentOrder = this.currentOrder.filter(c => c.number !== this.selectedCard.number);
|
||
|
||
// Debug: Check total card count
|
||
this.debugCardCount('after regular placement');
|
||
|
||
// Update slot appearance
|
||
slot.classList.add('filled');
|
||
// Reset to white background when filled
|
||
slot.style.background = '#fff';
|
||
slot.style.color = '#333';
|
||
slot.style.borderColor = '#2c5f76';
|
||
slot.innerHTML = `
|
||
<div class="slot-card">
|
||
<div class="card-svg">${this.selectedCard.svg}</div>
|
||
</div>
|
||
<div class="slot-label">← Click to move back</div>
|
||
`;
|
||
|
||
// Clear selection and re-render
|
||
this.clearSelection();
|
||
this.renderAvailableCards();
|
||
|
||
// Update status
|
||
const placedCount = this.placedCards.filter(c => c !== null).length;
|
||
if (placedCount === this.selectedCount) {
|
||
this.updateSortingStatus('All cards placed! Click "Check My Solution" to see how you did.');
|
||
} else {
|
||
this.updateSortingStatus(`${placedCount}/${this.selectedCount} cards placed. Select another card to continue.`);
|
||
}
|
||
}
|
||
|
||
handleDragEnd(e) {
|
||
if (this.previewOrder.length > 0) {
|
||
// Commit the preview order to the actual order
|
||
this.currentOrder = [...this.previewOrder];
|
||
}
|
||
|
||
// Clean up
|
||
this.resetDragState();
|
||
|
||
// Re-render with final positions
|
||
this.renderSortingCards();
|
||
|
||
// Add success animation
|
||
setTimeout(() => {
|
||
document.querySelectorAll('.sort-card').forEach(card => {
|
||
card.classList.add('success');
|
||
setTimeout(() => card.classList.remove('success'), 600);
|
||
});
|
||
}, 100);
|
||
}
|
||
|
||
handleDragOver(e) {
|
||
if (e.preventDefault) e.preventDefault();
|
||
e.dataTransfer.dropEffect = 'move';
|
||
|
||
// Calculate where to insert based on mouse position
|
||
const insertIndex = this.calculateInsertIndex(e.clientX, e.clientY);
|
||
console.log('DragOver - insertIndex:', insertIndex, 'lastInsertIndex:', this.lastInsertIndex);
|
||
if (insertIndex !== -1 && insertIndex !== this.lastInsertIndex) {
|
||
console.log('Calling updatePreviewOrder with insertIndex:', insertIndex);
|
||
this.updatePreviewOrder(insertIndex);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
handleDragEnter(e) {
|
||
// Handle drag enter for the sorting area
|
||
}
|
||
|
||
handleDragLeave(e) {
|
||
// Handle drag leave
|
||
}
|
||
|
||
handleDrop(e) {
|
||
if (e.stopPropagation) e.stopPropagation();
|
||
// The preview order is already set, just let dragEnd handle the commit
|
||
return false;
|
||
}
|
||
|
||
calculateInsertIndex(clientX, clientY) {
|
||
const sortingArea = document.getElementById('sorting-area');
|
||
const cards = Array.from(sortingArea.querySelectorAll('.sort-card:not(.dragging)'));
|
||
|
||
if (cards.length === 0) return 0;
|
||
|
||
|
||
// Simple approach: find which card the mouse is over
|
||
for (let i = 0; i < cards.length; i++) {
|
||
const card = cards[i];
|
||
const rect = card.getBoundingClientRect();
|
||
|
||
// Check if mouse is over this card
|
||
if (clientX >= rect.left && clientX <= rect.right &&
|
||
clientY >= rect.top && clientY <= rect.bottom) {
|
||
|
||
const cardIndex = parseInt(card.dataset.index);
|
||
const cardCenterX = rect.left + rect.width / 2;
|
||
|
||
const insertIndex = clientX < cardCenterX ? cardIndex : cardIndex + 1;
|
||
return insertIndex;
|
||
}
|
||
}
|
||
|
||
return this.currentOrder.length;
|
||
}
|
||
|
||
updatePreviewOrder(insertIndex) {
|
||
if (this.lastInsertIndex === insertIndex) return; // No change needed
|
||
this.lastInsertIndex = insertIndex;
|
||
|
||
console.log('updatePreviewOrder called with insertIndex:', insertIndex);
|
||
console.log('currentOrder:', this.currentOrder.map(c => c.number));
|
||
console.log('draggedIndex:', this.draggedIndex);
|
||
|
||
// Create new preview order
|
||
const newOrder = [...this.currentOrder];
|
||
const draggedCard = newOrder.splice(this.draggedIndex, 1)[0];
|
||
|
||
// Adjust insert index if we removed an item before it
|
||
let adjustedInsertIndex = insertIndex;
|
||
if (this.draggedIndex < insertIndex) {
|
||
adjustedInsertIndex--;
|
||
}
|
||
|
||
// Insert at new position
|
||
newOrder.splice(adjustedInsertIndex, 0, draggedCard);
|
||
this.previewOrder = newOrder;
|
||
|
||
console.log('New preview order:', this.previewOrder.map(c => c.number));
|
||
|
||
// Update the visual layout immediately
|
||
this.renderPreview();
|
||
}
|
||
|
||
renderPreview() {
|
||
console.log('renderPreview called');
|
||
const sortingArea = document.getElementById('sorting-area');
|
||
|
||
// Get all cards except the dragged one
|
||
const cards = Array.from(sortingArea.querySelectorAll('.sort-card:not(.dragging)'));
|
||
console.log('Found cards (excluding dragged):', cards.length);
|
||
|
||
const draggedNumber = parseInt(this.draggedElement.dataset.number);
|
||
|
||
// Simply compare the full orders - if they're different, reorder
|
||
const previewNumbers = this.previewOrder.map(c => c.number);
|
||
const currentNumbers = this.currentOrder.map(c => c.number);
|
||
|
||
console.log('Preview order:', previewNumbers);
|
||
console.log('Current order:', currentNumbers);
|
||
|
||
// Check if the orders are different
|
||
const needsReorder = !previewNumbers.every((num, index) => currentNumbers[index] === num);
|
||
|
||
console.log('Needs reorder:', needsReorder);
|
||
|
||
if (needsReorder) {
|
||
console.log('Performing reorder...');
|
||
|
||
// Update currentOrder to match preview (this is key!)
|
||
this.currentOrder = [...this.previewOrder];
|
||
|
||
// Create a simple reordering without removing cards from DOM
|
||
// Just change their visual order using CSS flexbox order or reinsert in correct order
|
||
|
||
const previewWithoutDragged = this.previewOrder.filter(card => card.number !== draggedNumber);
|
||
console.log('Cards to reorder:', previewWithoutDragged.map(c => c.number));
|
||
console.log('Available DOM cards:', cards.map(c => parseInt(c.dataset.number)));
|
||
|
||
// Store references to all current cards
|
||
const cardElements = new Map();
|
||
cards.forEach(card => {
|
||
cardElements.set(parseInt(card.dataset.number), card);
|
||
});
|
||
|
||
// Reorder by moving each card to the correct position
|
||
previewWithoutDragged.forEach((card, targetIndex) => {
|
||
const cardEl = cardElements.get(card.number);
|
||
console.log('Finding card', card.number, 'found element:', !!cardEl);
|
||
if (cardEl) {
|
||
cardEl.dataset.index = targetIndex;
|
||
cardEl.querySelector('.card-position').textContent = targetIndex + 1;
|
||
|
||
// Move to correct position in DOM
|
||
const currentIndex = Array.from(sortingArea.children).indexOf(cardEl);
|
||
const targetPosition = targetIndex;
|
||
|
||
if (currentIndex !== targetPosition) {
|
||
if (targetPosition >= sortingArea.children.length - 1) {
|
||
sortingArea.appendChild(cardEl);
|
||
} else {
|
||
const nextSibling = sortingArea.children[targetPosition];
|
||
sortingArea.insertBefore(cardEl, nextSibling);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Ensure dragged element stays at the end for z-index
|
||
if (this.draggedElement && this.draggedElement.parentNode === sortingArea) {
|
||
sortingArea.appendChild(this.draggedElement);
|
||
}
|
||
}
|
||
}
|
||
|
||
resetDragState() {
|
||
if (this.draggedElement) {
|
||
this.draggedElement.classList.remove('dragging');
|
||
this.draggedElement.style.transform = '';
|
||
this.draggedElement.style.position = '';
|
||
this.draggedElement.style.pointerEvents = '';
|
||
}
|
||
this.isDragging = false;
|
||
this.draggedElement = null;
|
||
this.draggedIndex = -1;
|
||
this.previewOrder = [];
|
||
this.lastInsertIndex = -1;
|
||
}
|
||
|
||
// Enhanced touch support for mobile with real-time reordering
|
||
handleTouchStart(e) {
|
||
const touch = e.touches[0];
|
||
this.touchStartX = touch.clientX;
|
||
this.touchStartY = touch.clientY;
|
||
this.touchStartElement = e.target.closest('.sort-card');
|
||
this.touchStartIndex = this.touchStartElement ? parseInt(this.touchStartElement.dataset.index) : -1;
|
||
|
||
if (this.touchStartElement) {
|
||
this.isDragging = true;
|
||
this.draggedElement = this.touchStartElement;
|
||
this.draggedIndex = this.touchStartIndex;
|
||
this.previewOrder = [...this.currentOrder];
|
||
this.lastInsertIndex = -1;
|
||
|
||
this.touchStartElement.classList.add('dragging');
|
||
this.createPlaceholder();
|
||
}
|
||
}
|
||
|
||
handleTouchMove(e) {
|
||
e.preventDefault();
|
||
|
||
if (this.touchStartElement && this.isDragging) {
|
||
const touch = e.touches[0];
|
||
|
||
// Update dragged element position
|
||
const deltaX = touch.clientX - this.touchStartX;
|
||
const deltaY = touch.clientY - this.touchStartY;
|
||
this.touchStartElement.style.transform = `translate(${deltaX}px, ${deltaY}px) rotate(5deg) scale(1.05)`;
|
||
|
||
// Calculate insert position and update preview
|
||
const insertIndex = this.calculateInsertIndex(touch.clientX, touch.clientY);
|
||
if (insertIndex !== -1) {
|
||
this.updatePreviewOrder(insertIndex);
|
||
}
|
||
}
|
||
}
|
||
|
||
handleTouchEnd(e) {
|
||
if (!this.touchStartElement || !this.isDragging) return;
|
||
|
||
// Commit the preview order
|
||
this.currentOrder = [...this.previewOrder];
|
||
|
||
// Clean up
|
||
this.touchStartElement.classList.remove('dragging');
|
||
this.touchStartElement.style.transform = '';
|
||
this.removePlaceholder();
|
||
|
||
this.isDragging = false;
|
||
this.draggedElement = null;
|
||
this.draggedIndex = -1;
|
||
this.previewOrder = [];
|
||
this.lastInsertIndex = -1;
|
||
|
||
// Re-render final positions
|
||
this.renderSortingCards();
|
||
|
||
// Success animation
|
||
setTimeout(() => {
|
||
document.querySelectorAll('.sort-card').forEach(card => {
|
||
card.classList.add('success');
|
||
setTimeout(() => card.classList.remove('success'), 600);
|
||
});
|
||
}, 100);
|
||
|
||
this.touchStartElement = null;
|
||
this.touchStartIndex = -1;
|
||
}
|
||
|
||
checkSolution() {
|
||
if (this.placedCards.some(card => card === null)) {
|
||
this.updateSortingStatus('Please place all cards before checking your solution.');
|
||
return;
|
||
}
|
||
|
||
// Get the sequences for comparison
|
||
const userSequence = this.placedCards.map(card => card.number);
|
||
const correctSequence = this.correctOrder.map(card => card.number);
|
||
|
||
// Calculate fair score using sequence alignment
|
||
const scoreResult = this.calculateSequenceScore(userSequence, correctSequence);
|
||
|
||
// Update visual feedback for each position
|
||
this.placedCards.forEach((card, index) => {
|
||
const slot = document.querySelector(`[data-position="${index}"]`);
|
||
const isExactMatch = card.number === this.correctOrder[index].number;
|
||
|
||
if (isExactMatch) {
|
||
slot.classList.add('correct');
|
||
slot.classList.remove('incorrect');
|
||
} else {
|
||
slot.classList.add('incorrect');
|
||
slot.classList.remove('correct');
|
||
}
|
||
});
|
||
|
||
this.showAdvancedFeedback(userSequence, correctSequence);
|
||
}
|
||
|
||
calculateSequenceScore(userSeq, correctSeq) {
|
||
// Calculate Longest Common Subsequence (LCS) score
|
||
const lcsLength = this.longestCommonSubsequence(userSeq, correctSeq);
|
||
|
||
// Calculate how many cards are in correct relative order
|
||
const relativeOrderScore = (lcsLength / correctSeq.length) * 100;
|
||
|
||
// Calculate exact position matches
|
||
let exactMatches = 0;
|
||
for (let i = 0; i < userSeq.length; i++) {
|
||
if (userSeq[i] === correctSeq[i]) {
|
||
exactMatches++;
|
||
}
|
||
}
|
||
const exactScore = (exactMatches / correctSeq.length) * 100;
|
||
|
||
// Calculate inversion count (how "scrambled" the sequence is)
|
||
const inversions = this.countInversions(userSeq, correctSeq);
|
||
const maxInversions = (correctSeq.length * (correctSeq.length - 1)) / 2;
|
||
const inversionScore = Math.max(0, ((maxInversions - inversions) / maxInversions) * 100);
|
||
|
||
// Weighted final score:
|
||
// - 50% for having cards in correct relative order (LCS)
|
||
// - 30% for exact position matches
|
||
// - 20% for overall sequence organization (inversions)
|
||
const finalScore = Math.round(
|
||
(relativeOrderScore * 0.5) +
|
||
(exactScore * 0.3) +
|
||
(inversionScore * 0.2)
|
||
);
|
||
|
||
return {
|
||
percentage: finalScore,
|
||
exactMatches: exactMatches,
|
||
correctRelativeOrder: lcsLength,
|
||
totalCards: correctSeq.length,
|
||
details: {
|
||
relativeOrderScore: Math.round(relativeOrderScore),
|
||
exactScore: Math.round(exactScore),
|
||
inversionScore: Math.round(inversionScore),
|
||
inversions: inversions
|
||
}
|
||
};
|
||
}
|
||
|
||
longestCommonSubsequence(seq1, seq2) {
|
||
const m = seq1.length;
|
||
const n = seq2.length;
|
||
const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
|
||
|
||
for (let i = 1; i <= m; i++) {
|
||
for (let j = 1; j <= n; j++) {
|
||
if (seq1[i - 1] === seq2[j - 1]) {
|
||
dp[i][j] = dp[i - 1][j - 1] + 1;
|
||
} else {
|
||
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||
}
|
||
}
|
||
}
|
||
|
||
return dp[m][n];
|
||
}
|
||
|
||
countInversions(userSeq, correctSeq) {
|
||
// Create a mapping from value to correct position
|
||
const correctPositions = {};
|
||
correctSeq.forEach((val, idx) => {
|
||
correctPositions[val] = idx;
|
||
});
|
||
|
||
// Convert user sequence to "correct position" sequence
|
||
const userCorrectPositions = userSeq.map(val => correctPositions[val]);
|
||
|
||
// Count inversions in this position sequence
|
||
let inversions = 0;
|
||
for (let i = 0; i < userCorrectPositions.length; i++) {
|
||
for (let j = i + 1; j < userCorrectPositions.length; j++) {
|
||
if (userCorrectPositions[i] > userCorrectPositions[j]) {
|
||
inversions++;
|
||
}
|
||
}
|
||
}
|
||
|
||
return inversions;
|
||
}
|
||
|
||
showAdvancedFeedback(userSeq, correctSeq) {
|
||
const feedbackEl = document.getElementById('sorting-feedback');
|
||
|
||
// Calculate advanced scoring metrics
|
||
const lcsLength = this.longestCommonSubsequence(userSeq, correctSeq);
|
||
const relativeOrderScore = (lcsLength / correctSeq.length) * 100;
|
||
|
||
// Calculate exact position matches
|
||
let exactMatches = 0;
|
||
for (let i = 0; i < Math.min(userSeq.length, correctSeq.length); i++) {
|
||
if (userSeq[i] === correctSeq[i]) {
|
||
exactMatches++;
|
||
}
|
||
}
|
||
const exactScore = (exactMatches / correctSeq.length) * 100;
|
||
|
||
// Calculate inversion score
|
||
const inversions = this.countInversions(userSeq, correctSeq);
|
||
const maxInversions = (correctSeq.length * (correctSeq.length - 1)) / 2;
|
||
const inversionScore = Math.max(0, ((maxInversions - inversions) / maxInversions) * 100);
|
||
|
||
// Weighted final score
|
||
const finalScore = Math.round(
|
||
(relativeOrderScore * 0.5) +
|
||
(exactScore * 0.3) +
|
||
(inversionScore * 0.2)
|
||
);
|
||
|
||
const isPerfect = finalScore === 100;
|
||
|
||
let feedbackClass, message;
|
||
|
||
if (isPerfect) {
|
||
feedbackClass = 'feedback-perfect';
|
||
message = '🎉 Perfect! All cards in correct order!';
|
||
} else if (finalScore >= 80) {
|
||
feedbackClass = 'feedback-good';
|
||
message = '👍 Excellent! Very close to perfect!';
|
||
} else if (finalScore >= 60) {
|
||
feedbackClass = 'feedback-good';
|
||
message = '👍 Good job! You understand the pattern!';
|
||
} else {
|
||
feedbackClass = 'feedback-needs-work';
|
||
message = '💪 Keep practicing! Focus on reading each abacus carefully.';
|
||
}
|
||
|
||
// Show score in modal instead of inline
|
||
this.showScoreModal({
|
||
finalScore,
|
||
message,
|
||
relativeOrderScore,
|
||
exactScore,
|
||
inversionScore,
|
||
lcsLength,
|
||
correctSeq,
|
||
exactMatches,
|
||
feedbackClass,
|
||
elapsedTime: this.getElapsedTime()
|
||
});
|
||
|
||
// Keep inline feedback hidden
|
||
feedbackEl.style.display = 'none';
|
||
|
||
this.updateSortingStatus(isPerfect ? 'Perfect solution!' : `${finalScore}% score`);
|
||
}
|
||
|
||
revealNumbers() {
|
||
// Track revealed state
|
||
this.numbersRevealed = true;
|
||
|
||
document.querySelectorAll('.sort-card').forEach(card => {
|
||
card.classList.add('revealed');
|
||
});
|
||
|
||
document.getElementById('reveal-numbers').style.display = 'none';
|
||
this.updateSortingStatus('Numbers revealed - now you can see the correct order!');
|
||
}
|
||
|
||
startTimer() {
|
||
this.startTime = Date.now();
|
||
this.timerInterval = setInterval(() => {
|
||
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
||
const minutes = Math.floor(elapsed / 60);
|
||
const seconds = elapsed % 60;
|
||
document.getElementById('sorting-timer').textContent =
|
||
`${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||
}, 1000);
|
||
}
|
||
|
||
stopTimer() {
|
||
if (this.timerInterval) {
|
||
clearInterval(this.timerInterval);
|
||
this.timerInterval = null;
|
||
}
|
||
}
|
||
|
||
getElapsedTime() {
|
||
if (this.startTime) {
|
||
return Math.floor((Date.now() - this.startTime) / 1000);
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
showScoreModal(scoreData) {
|
||
const {
|
||
finalScore, message, relativeOrderScore, exactScore, inversionScore,
|
||
lcsLength, correctSeq, exactMatches, feedbackClass, elapsedTime
|
||
} = scoreData;
|
||
|
||
const minutes = Math.floor(elapsedTime / 60);
|
||
const seconds = elapsedTime % 60;
|
||
const timeDisplay = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||
|
||
document.getElementById('score-modal-body').innerHTML = `
|
||
<div class="feedback-score">${finalScore}%</div>
|
||
<div class="feedback-message">${message}</div>
|
||
<div class="feedback-time">⏱️ Time: ${timeDisplay}</div>
|
||
<div class="feedback-breakdown">
|
||
<div class="score-component">
|
||
<span class="component-label">Sequence Order:</span>
|
||
<span class="component-score">${Math.round(relativeOrderScore)}%</span>
|
||
<span class="component-weight">(50% weight)</span>
|
||
</div>
|
||
<div class="score-component">
|
||
<span class="component-label">Exact Positions:</span>
|
||
<span class="component-score">${Math.round(exactScore)}%</span>
|
||
<span class="component-weight">(30% weight)</span>
|
||
</div>
|
||
<div class="score-component">
|
||
<span class="component-label">Organization:</span>
|
||
<span class="component-score">${Math.round(inversionScore)}%</span>
|
||
<span class="component-weight">(20% weight)</span>
|
||
</div>
|
||
</div>
|
||
<div class="feedback-details">
|
||
Cards in correct order: ${lcsLength}/${correctSeq.length} •
|
||
Exact position matches: ${exactMatches}/${correctSeq.length}
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('score-modal').style.display = 'flex';
|
||
}
|
||
|
||
hideScoreModal() {
|
||
document.getElementById('score-modal').style.display = 'none';
|
||
}
|
||
|
||
endSorting() {
|
||
// End the current sorting and return to configuration
|
||
this.resetSorting();
|
||
}
|
||
|
||
resetSorting() {
|
||
// Reset state
|
||
this.currentOrder = [];
|
||
this.correctOrder = [];
|
||
this.placedCards = [];
|
||
this.selectedCard = null;
|
||
this.selectedCardElement = null;
|
||
|
||
// Clear feedback
|
||
document.getElementById('sorting-feedback').style.display = 'none';
|
||
|
||
// Hide game and sticky header, show configuration controls
|
||
document.getElementById('sorting-game').style.display = 'none';
|
||
document.getElementById('sorting-header').style.display = 'none';
|
||
document.querySelector('.sorting-controls').style.display = 'block';
|
||
|
||
// Stop timer
|
||
this.stopTimer();
|
||
|
||
// Reset buttons
|
||
document.getElementById('start-sorting').style.display = 'inline-block';
|
||
document.querySelector('.sorting-game-actions').style.display = 'none';
|
||
}
|
||
|
||
newChallenge() {
|
||
// Reset state but stay in game mode
|
||
this.currentOrder = [];
|
||
this.correctOrder = [];
|
||
this.placedCards = [];
|
||
this.selectedCard = null;
|
||
this.selectedCardElement = null;
|
||
this.numbersRevealed = false;
|
||
|
||
// Clear feedback
|
||
document.getElementById('sorting-feedback').style.display = 'none';
|
||
|
||
// Clear card states
|
||
document.querySelectorAll('.sort-card').forEach(card => {
|
||
card.classList.remove('correct', 'incorrect', 'revealed');
|
||
});
|
||
|
||
// Start a new challenge (keeping game mode active)
|
||
this.startSorting();
|
||
}
|
||
|
||
updateSortingStatus(message) {
|
||
document.getElementById('sorting-status').textContent = message;
|
||
}
|
||
}
|
||
|
||
// Matching Pairs Challenge
|
||
class MatchingChallenge {
|
||
constructor() {
|
||
this.cards = [];
|
||
this.gameCards = [];
|
||
this.flippedCards = [];
|
||
this.matchedPairs = 0;
|
||
this.totalPairs = 0;
|
||
this.moves = 0;
|
||
this.selectedPairs = 6;
|
||
this.gameStartTime = null;
|
||
this.timerInterval = null;
|
||
|
||
// Two-player mode variables
|
||
this.gameMode = 'single';
|
||
this.turnTimer = 0; // 0 = no timer
|
||
this.currentPlayer = 1;
|
||
this.player1Score = 0;
|
||
this.player2Score = 0;
|
||
this.turnTimeLeft = 0;
|
||
this.turnTimerInterval = null;
|
||
this.playerTurnActive = true;
|
||
this.isFirstMove = true;
|
||
|
||
this.initializeMatching();
|
||
this.bindMatchingEvents();
|
||
}
|
||
|
||
initializeMatching() {
|
||
// Get available cards (same as quiz cards)
|
||
const cardElements = document.querySelectorAll('.flashcard');
|
||
this.cards = Array.from(cardElements).map(card => ({
|
||
number: parseInt(card.dataset.number),
|
||
svg: card.querySelector('.abacus-container').outerHTML
|
||
}));
|
||
}
|
||
|
||
bindMatchingEvents() {
|
||
// Remove existing event listeners to prevent duplicates
|
||
document.querySelectorAll('.mode-btn').forEach(btn => {
|
||
// Clone the button to remove all event listeners
|
||
const newBtn = btn.cloneNode(true);
|
||
btn.parentNode.replaceChild(newBtn, btn);
|
||
});
|
||
|
||
document.querySelectorAll('.timer-btn').forEach(btn => {
|
||
// Clone the button to remove all event listeners
|
||
const newBtn = btn.cloneNode(true);
|
||
btn.parentNode.replaceChild(newBtn, btn);
|
||
});
|
||
|
||
document.querySelectorAll('.start-game-btn').forEach(btn => {
|
||
// Clone the button to remove all event listeners
|
||
const newBtn = btn.cloneNode(true);
|
||
btn.parentNode.replaceChild(newBtn, btn);
|
||
});
|
||
|
||
// Mode selection
|
||
document.querySelectorAll('.mode-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
|
||
e.target.classList.add('active');
|
||
this.gameMode = e.target.dataset.mode;
|
||
|
||
// Show/hide timer controls for two-player mode
|
||
const timerControls = document.getElementById('timer-controls');
|
||
if (this.gameMode === 'two-player') {
|
||
timerControls.style.display = 'block';
|
||
} else {
|
||
timerControls.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
|
||
// Timer selection
|
||
document.querySelectorAll('.timer-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
document.querySelectorAll('.timer-btn').forEach(b => b.classList.remove('active'));
|
||
e.target.classList.add('active');
|
||
this.turnTimer = parseInt(e.target.dataset.timer);
|
||
});
|
||
});
|
||
|
||
// Grid size selection and immediate game start
|
||
document.querySelectorAll('.start-game-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
// Get the pairs count from the clicked button
|
||
const target = e.currentTarget; // Use currentTarget to get the button itself
|
||
this.selectedPairs = parseInt(target.dataset.pairs);
|
||
|
||
// Start the game immediately
|
||
this.startMatching();
|
||
});
|
||
});
|
||
|
||
// Action buttons
|
||
document.getElementById('new-matching').addEventListener('click', () => this.newChallenge());
|
||
document.getElementById('end-matching').addEventListener('click', () => this.endMatching());
|
||
|
||
// Score modal event listeners
|
||
document.querySelector('.matching-score-modal-close').addEventListener('click', () => this.hideScoreModal());
|
||
document.getElementById('matching-score-modal-close-btn').addEventListener('click', () => this.hideScoreModal());
|
||
document.getElementById('matching-score-modal-new-game').addEventListener('click', () => {
|
||
this.hideScoreModal();
|
||
this.newChallenge();
|
||
});
|
||
|
||
// Close modal when clicking outside
|
||
document.getElementById('matching-score-modal').addEventListener('click', (e) => {
|
||
if (e.target.id === 'matching-score-modal') {
|
||
this.hideScoreModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
startMatching() {
|
||
// Select random cards for matching
|
||
const shuffledCards = [...this.cards].sort(() => Math.random() - 0.5);
|
||
const selectedCards = shuffledCards.slice(0, this.selectedPairs);
|
||
|
||
// Create pairs: abacus and number
|
||
this.gameCards = [];
|
||
selectedCards.forEach(card => {
|
||
// Add abacus card
|
||
this.gameCards.push({
|
||
id: `abacus_${card.number}`,
|
||
type: 'abacus',
|
||
number: card.number,
|
||
content: card.svg,
|
||
matched: false
|
||
});
|
||
|
||
// Add number card
|
||
this.gameCards.push({
|
||
id: `number_${card.number}`,
|
||
type: 'number',
|
||
number: card.number,
|
||
content: card.number.toString(),
|
||
matched: false
|
||
});
|
||
});
|
||
|
||
// Shuffle the game cards
|
||
this.gameCards = this.gameCards.sort(() => Math.random() - 0.5);
|
||
this.totalPairs = this.selectedPairs;
|
||
this.matchedPairs = 0;
|
||
this.moves = 0;
|
||
this.flippedCards = [];
|
||
|
||
// Initialize two-player mode variables
|
||
if (this.gameMode === 'two-player') {
|
||
this.currentPlayer = 1;
|
||
this.player1Score = 0;
|
||
this.player2Score = 0;
|
||
this.playerTurnActive = true;
|
||
}
|
||
|
||
// Hide configuration controls, show game
|
||
document.querySelector('.matching-controls').style.display = 'none';
|
||
document.getElementById('matching-game').style.display = 'block';
|
||
document.getElementById('matching-header').style.display = 'block';
|
||
|
||
this.renderMatchingGrid();
|
||
this.adjustCardSizes();
|
||
this.startTimer();
|
||
this.setupGameInterface();
|
||
this.updateMatchingStatus(this.gameMode === 'two-player' ? 'Player 1 starts!' : 'Find all matching pairs!');
|
||
this.updateMatchingStats();
|
||
|
||
// For two-player mode with timer, show timer but don't start counting until first move
|
||
if (this.gameMode === 'two-player' && this.turnTimer > 0) {
|
||
this.turnTimeLeft = this.turnTimer;
|
||
this.updateTurnTimerDisplay();
|
||
this.updateTurnTimerStatus('waiting'); // Show timer is waiting for first move
|
||
}
|
||
}
|
||
|
||
renderMatchingGrid() {
|
||
const grid = document.getElementById('matching-grid');
|
||
|
||
// Set grid class based on number of pairs
|
||
grid.className = 'matching-grid';
|
||
if (this.selectedPairs === 6) grid.classList.add('grid-3x4');
|
||
else if (this.selectedPairs === 8) grid.classList.add('grid-4x4');
|
||
else if (this.selectedPairs === 12) grid.classList.add('grid-4x6');
|
||
else if (this.selectedPairs === 15) grid.classList.add('grid-5x6');
|
||
|
||
grid.innerHTML = '';
|
||
|
||
this.gameCards.forEach((card, index) => {
|
||
const cardElement = document.createElement('div');
|
||
cardElement.className = 'match-card';
|
||
cardElement.dataset.index = index;
|
||
cardElement.dataset.cardId = card.id;
|
||
cardElement.dataset.number = card.number;
|
||
|
||
const typeClass = card.type === 'abacus' ? 'abacus-type' : 'number-type';
|
||
const typeIcon = card.type === 'abacus' ? '🧮' : '🔢';
|
||
|
||
cardElement.innerHTML = `
|
||
<div class="match-card-back ${typeClass}">
|
||
<div class="card-type-icon">${typeIcon}</div>
|
||
</div>
|
||
<div class="match-card-content">
|
||
${card.type === 'number' ?
|
||
`<div class="match-card-number">${card.content}</div>` :
|
||
`<div class="match-card-abacus">${card.content}</div>`
|
||
}
|
||
</div>
|
||
`;
|
||
|
||
cardElement.addEventListener('click', () => this.cardClicked(index));
|
||
grid.appendChild(cardElement);
|
||
});
|
||
}
|
||
|
||
cardClicked(index) {
|
||
const card = this.gameCards[index];
|
||
const cardElement = document.querySelector(`[data-index="${index}"]`);
|
||
|
||
// Ignore if card is already flipped or matched
|
||
if (card.matched || cardElement.classList.contains('flipped')) return;
|
||
|
||
// Ignore if two cards are already flipped
|
||
if (this.flippedCards.length >= 2) return;
|
||
|
||
// In two-player mode, ignore clicks if it's not the player's turn
|
||
if (this.gameMode === 'two-player' && !this.playerTurnActive) return;
|
||
|
||
// Start timer on first move in two-player mode
|
||
if (this.gameMode === 'two-player' && this.turnTimer > 0 && this.isFirstMove) {
|
||
this.isFirstMove = false;
|
||
this.startTurnTimer();
|
||
this.updateTurnTimerStatus('active');
|
||
}
|
||
|
||
// If one card is already flipped, only allow clicking cards of opposite type
|
||
if (this.flippedCards.length === 1) {
|
||
const flippedCardType = this.flippedCards[0].card.type;
|
||
if (card.type === flippedCardType) {
|
||
// Same type - show visual feedback but don't flip
|
||
this.showInvalidMoveHint(cardElement);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Flip the card
|
||
cardElement.classList.add('flipped');
|
||
this.flippedCards.push({ index, element: cardElement, card });
|
||
|
||
// If this is the first card flipped, highlight valid choices
|
||
if (this.flippedCards.length === 1) {
|
||
this.highlightValidCards(card.type);
|
||
}
|
||
|
||
// If two cards are flipped, check for match
|
||
if (this.flippedCards.length === 2) {
|
||
this.clearHighlights();
|
||
this.stopTurnTimer();
|
||
|
||
if (this.gameMode === 'single') {
|
||
this.moves++;
|
||
}
|
||
|
||
this.updateMatchingStats();
|
||
setTimeout(() => this.checkMatch(), 1000);
|
||
}
|
||
}
|
||
|
||
checkMatch() {
|
||
const [first, second] = this.flippedCards;
|
||
|
||
// Check if numbers match and types are different
|
||
if (first.card.number === second.card.number && first.card.type !== second.card.type) {
|
||
// Match found!
|
||
first.element.classList.add('matched');
|
||
second.element.classList.add('matched');
|
||
first.card.matched = true;
|
||
second.card.matched = true;
|
||
|
||
this.matchedPairs++;
|
||
|
||
// In two-player mode, award point to current player and mark cards
|
||
if (this.gameMode === 'two-player') {
|
||
const playerClass = `matched-player${this.currentPlayer}`;
|
||
first.element.classList.add(playerClass);
|
||
second.element.classList.add(playerClass);
|
||
|
||
// Add player badges
|
||
this.addPlayerBadge(first.element, this.currentPlayer);
|
||
this.addPlayerBadge(second.element, this.currentPlayer);
|
||
|
||
if (this.currentPlayer === 1) {
|
||
this.player1Score++;
|
||
} else {
|
||
this.player2Score++;
|
||
}
|
||
// Player gets another turn after a match
|
||
this.updatePlayerStats();
|
||
} else {
|
||
this.updateMatchingStats();
|
||
}
|
||
|
||
// Check if game is complete
|
||
if (this.matchedPairs === this.totalPairs) {
|
||
this.endGame();
|
||
}
|
||
} else {
|
||
// No match - flip cards back
|
||
first.element.classList.remove('flipped');
|
||
second.element.classList.remove('flipped');
|
||
|
||
// In two-player mode, switch turns after a miss
|
||
if (this.gameMode === 'two-player') {
|
||
this.switchPlayer();
|
||
}
|
||
}
|
||
|
||
this.flippedCards = [];
|
||
this.clearHighlights();
|
||
this.playerTurnActive = true; // Re-enable clicks
|
||
}
|
||
|
||
endGame() {
|
||
this.stopTimer();
|
||
this.stopTurnTimer();
|
||
const elapsedTime = Date.now() - this.gameStartTime;
|
||
const seconds = Math.floor(elapsedTime / 1000);
|
||
|
||
if (this.gameMode === 'two-player') {
|
||
// Two-player results
|
||
let winner, description;
|
||
if (this.player1Score > this.player2Score) {
|
||
winner = 'Player 1 Wins!';
|
||
description = `${this.player1Score} - ${this.player2Score}`;
|
||
} else if (this.player2Score > this.player1Score) {
|
||
winner = 'Player 2 Wins!';
|
||
description = `${this.player2Score} - ${this.player1Score}`;
|
||
} else {
|
||
winner = "It's a Tie!";
|
||
description = `${this.player1Score} - ${this.player2Score}`;
|
||
}
|
||
|
||
this.showScoreModal({
|
||
gameMode: 'two-player',
|
||
winner,
|
||
description,
|
||
player1Score: this.player1Score,
|
||
player2Score: this.player2Score,
|
||
totalPairs: this.totalPairs,
|
||
time: seconds
|
||
});
|
||
} else {
|
||
// Single player results
|
||
const minimumMoves = this.totalPairs;
|
||
const efficiency = this.moves / minimumMoves;
|
||
|
||
let medal, description;
|
||
if (efficiency <= 1.5) {
|
||
medal = '🏆 Gold';
|
||
description = 'Perfect efficiency!';
|
||
} else if (efficiency <= 2.0) {
|
||
medal = '🥈 Silver';
|
||
description = 'Great job!';
|
||
} else if (efficiency <= 3.0) {
|
||
medal = '🥉 Bronze';
|
||
description = 'Good effort!';
|
||
} else {
|
||
medal = '🎯 Practice';
|
||
description = 'Keep practicing!';
|
||
}
|
||
|
||
this.showScoreModal({
|
||
gameMode: 'single',
|
||
pairs: this.totalPairs,
|
||
moves: this.moves,
|
||
time: seconds,
|
||
efficiency: efficiency.toFixed(1),
|
||
medal,
|
||
description
|
||
});
|
||
}
|
||
}
|
||
|
||
showScoreModal(results) {
|
||
const modal = document.getElementById('matching-score-modal');
|
||
const body = document.getElementById('matching-score-modal-body');
|
||
|
||
const timeStr = this.formatTime(results.time);
|
||
|
||
if (results.gameMode === 'two-player') {
|
||
// Two-player results layout
|
||
body.innerHTML = `
|
||
<div class="score-summary">
|
||
<div class="score-medal">${results.winner}</div>
|
||
<div class="score-description">${results.description}</div>
|
||
</div>
|
||
|
||
<div class="two-player-results">
|
||
<div class="player-result">
|
||
<div class="player-name">🔵 Player 1</div>
|
||
<div class="player-final-score" style="color: #007bff;">${results.player1Score} pairs</div>
|
||
</div>
|
||
<div class="vs-divider">VS</div>
|
||
<div class="player-result">
|
||
<div class="player-name">🟠 Player 2</div>
|
||
<div class="player-final-score" style="color: #fd7e14;">${results.player2Score} pairs</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="game-summary">
|
||
<div class="summary-item">
|
||
<span class="summary-label">Total Pairs:</span>
|
||
<span class="summary-value">${results.totalPairs}</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="summary-label">Game Time:</span>
|
||
<span class="summary-value">${timeStr}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
// Single player results layout
|
||
body.innerHTML = `
|
||
<div class="score-summary">
|
||
<div class="score-medal">${results.medal}</div>
|
||
<div class="score-description">${results.description}</div>
|
||
</div>
|
||
|
||
<div class="score-details">
|
||
<div class="score-item">
|
||
<span class="score-label">Pairs Matched:</span>
|
||
<span class="score-value">${results.pairs}</span>
|
||
</div>
|
||
<div class="score-item">
|
||
<span class="score-label">Total Moves:</span>
|
||
<span class="score-value">${results.moves}</span>
|
||
</div>
|
||
<div class="score-item">
|
||
<span class="score-label">Time:</span>
|
||
<span class="score-value">${timeStr}</span>
|
||
</div>
|
||
<div class="score-item">
|
||
<span class="score-label">Efficiency:</span>
|
||
<span class="score-value">${results.efficiency}x</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="score-explanation">
|
||
<h4>Scoring System</h4>
|
||
<div class="score-tiers">
|
||
<div>🏆 Gold: ≤1.5x minimum moves</div>
|
||
<div>🥈 Silver: ≤2.0x minimum moves</div>
|
||
<div>🥉 Bronze: ≤3.0x minimum moves</div>
|
||
<div>🎯 Practice: >3.0x minimum moves</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
hideScoreModal() {
|
||
document.getElementById('matching-score-modal').style.display = 'none';
|
||
}
|
||
|
||
newChallenge() {
|
||
this.endMatching();
|
||
setTimeout(() => this.startMatching(), 100);
|
||
}
|
||
|
||
endMatching() {
|
||
// Stop timer
|
||
this.stopTimer();
|
||
this.stopTurnTimer();
|
||
|
||
// Hide game and sticky header, show configuration controls
|
||
document.getElementById('matching-game').style.display = 'none';
|
||
document.getElementById('matching-header').style.display = 'none';
|
||
document.querySelector('.matching-controls').style.display = 'block';
|
||
|
||
// Reset game state
|
||
this.flippedCards = [];
|
||
this.matchedPairs = 0;
|
||
this.moves = 0;
|
||
this.currentPlayer = 1;
|
||
this.player1Score = 0;
|
||
this.player2Score = 0;
|
||
this.playerTurnActive = true;
|
||
this.isFirstMove = true;
|
||
|
||
// Reset mode selection display but preserve selected values
|
||
this.updateModeDisplay();
|
||
|
||
// Re-bind events to ensure they work after DOM changes
|
||
this.bindMatchingEvents();
|
||
}
|
||
|
||
updateModeDisplay() {
|
||
// Ensure the current game mode button is properly highlighted
|
||
document.querySelectorAll('.mode-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
if (btn.dataset.mode === this.gameMode) {
|
||
btn.classList.add('active');
|
||
}
|
||
});
|
||
|
||
// Show/hide timer controls based on current mode
|
||
const timerControls = document.getElementById('timer-controls');
|
||
if (this.gameMode === 'two-player') {
|
||
timerControls.style.display = 'block';
|
||
} else {
|
||
timerControls.style.display = 'none';
|
||
}
|
||
|
||
// Ensure timer button is highlighted
|
||
document.querySelectorAll('.timer-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
if (parseInt(btn.dataset.timer) === this.turnTimer) {
|
||
btn.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
startTimer() {
|
||
this.gameStartTime = Date.now();
|
||
this.timerInterval = setInterval(() => {
|
||
const elapsed = Date.now() - this.gameStartTime;
|
||
const seconds = Math.floor(elapsed / 1000);
|
||
document.getElementById('matching-timer').textContent = this.formatTime(seconds);
|
||
}, 1000);
|
||
}
|
||
|
||
stopTimer() {
|
||
if (this.timerInterval) {
|
||
clearInterval(this.timerInterval);
|
||
this.timerInterval = null;
|
||
}
|
||
}
|
||
|
||
formatTime(totalSeconds) {
|
||
const minutes = Math.floor(totalSeconds / 60);
|
||
const seconds = totalSeconds % 60;
|
||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
updateMatchingStatus(message) {
|
||
document.getElementById('matching-status').textContent = message;
|
||
}
|
||
|
||
updateMatchingStats() {
|
||
document.getElementById('moves-counter').textContent = `Moves: ${this.moves}`;
|
||
document.getElementById('pairs-found').textContent = `Pairs: ${this.matchedPairs}/${this.totalPairs}`;
|
||
}
|
||
|
||
adjustCardSizes() {
|
||
// CSS now handles responsive sizing automatically
|
||
// This method can be kept for any future dynamic adjustments if needed
|
||
|
||
// Just ensure font sizes scale with container
|
||
const grid = document.getElementById('matching-grid');
|
||
const cards = grid.querySelectorAll('.match-card');
|
||
|
||
cards.forEach(card => {
|
||
const cardRect = card.getBoundingClientRect();
|
||
const cardSize = Math.min(cardRect.width, cardRect.height);
|
||
|
||
// Adjust font sizes proportionally to actual card size
|
||
const baseFontSize = Math.max(cardSize * 0.25, 12);
|
||
const iconSize = Math.max(cardSize * 0.4, 16);
|
||
|
||
const number = card.querySelector('.match-card-number');
|
||
if (number) number.style.fontSize = `${baseFontSize}px`;
|
||
|
||
const icon = card.querySelector('.card-type-icon');
|
||
if (icon) icon.style.fontSize = `${iconSize}px`;
|
||
});
|
||
}
|
||
|
||
showInvalidMoveHint(cardElement) {
|
||
// Add temporary visual feedback for invalid moves
|
||
cardElement.classList.add('invalid-move');
|
||
|
||
// Remove the class after animation
|
||
setTimeout(() => {
|
||
cardElement.classList.remove('invalid-move');
|
||
}, 600);
|
||
}
|
||
|
||
highlightValidCards(flippedCardType) {
|
||
// Highlight cards of the opposite type
|
||
const validType = flippedCardType === 'abacus' ? 'number' : 'abacus';
|
||
|
||
this.gameCards.forEach((card, index) => {
|
||
const cardElement = document.querySelector(`[data-index="${index}"]`);
|
||
if (!card.matched && !cardElement.classList.contains('flipped')) {
|
||
if (card.type === validType) {
|
||
cardElement.classList.add('valid-choice');
|
||
} else {
|
||
cardElement.classList.add('invalid-choice');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
clearHighlights() {
|
||
// Remove all highlight classes
|
||
const cards = document.querySelectorAll('.match-card');
|
||
cards.forEach(card => {
|
||
card.classList.remove('valid-choice', 'invalid-choice');
|
||
});
|
||
}
|
||
|
||
setupGameInterface() {
|
||
// Show/hide appropriate UI elements based on game mode
|
||
const singleStats = document.querySelector('.matching-stats');
|
||
const playerStats = document.getElementById('player-stats');
|
||
|
||
if (this.gameMode === 'two-player') {
|
||
singleStats.style.display = 'none';
|
||
playerStats.style.display = 'flex';
|
||
this.updatePlayerStats();
|
||
|
||
// Show turn timer if enabled
|
||
const turnTimer = document.getElementById('turn-timer');
|
||
if (this.turnTimer > 0) {
|
||
turnTimer.style.display = 'block';
|
||
turnTimer.textContent = `${this.turnTimer}s`;
|
||
} else {
|
||
turnTimer.style.display = 'none';
|
||
}
|
||
} else {
|
||
singleStats.style.display = 'flex';
|
||
playerStats.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
updatePlayerStats() {
|
||
// Update player scores and current turn indicator
|
||
document.querySelector('#player1-info .player-score').textContent = `${this.player1Score} pairs`;
|
||
document.querySelector('#player2-info .player-score').textContent = `${this.player2Score} pairs`;
|
||
|
||
// Update active player highlighting
|
||
const player1Info = document.getElementById('player1-info');
|
||
const player2Info = document.getElementById('player2-info');
|
||
const currentTurn = document.getElementById('current-turn');
|
||
|
||
if (this.currentPlayer === 1) {
|
||
player1Info.classList.add('active');
|
||
player2Info.classList.remove('active');
|
||
currentTurn.textContent = "🔵 Player 1's Turn";
|
||
} else {
|
||
player1Info.classList.remove('active');
|
||
player2Info.classList.add('active');
|
||
currentTurn.textContent = "🟠 Player 2's Turn";
|
||
}
|
||
}
|
||
|
||
addPlayerBadge(cardElement, player) {
|
||
// Create player badge element
|
||
const badge = document.createElement('div');
|
||
badge.className = `player-badge player${player}`;
|
||
badge.textContent = player;
|
||
|
||
// Add badge to card
|
||
cardElement.appendChild(badge);
|
||
}
|
||
|
||
switchPlayer() {
|
||
this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
|
||
this.updatePlayerStats();
|
||
this.updateMatchingStatus(`Player ${this.currentPlayer}'s turn`);
|
||
|
||
// Start turn timer immediately for new player if enabled
|
||
if (this.turnTimer > 0) {
|
||
this.startTurnTimer();
|
||
}
|
||
}
|
||
|
||
startTurnTimer() {
|
||
if (this.turnTimer <= 0) return;
|
||
|
||
this.turnTimeLeft = this.turnTimer;
|
||
this.updateTurnTimerDisplay();
|
||
|
||
this.turnTimerInterval = setInterval(() => {
|
||
this.turnTimeLeft--;
|
||
this.updateTurnTimerDisplay();
|
||
|
||
if (this.turnTimeLeft <= 0) {
|
||
this.handleTurnTimeout();
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
stopTurnTimer() {
|
||
if (this.turnTimerInterval) {
|
||
clearInterval(this.turnTimerInterval);
|
||
this.turnTimerInterval = null;
|
||
}
|
||
}
|
||
|
||
updateTurnTimerDisplay() {
|
||
const timerElement = document.getElementById('turn-timer');
|
||
if (!timerElement || this.turnTimer <= 0) return;
|
||
|
||
timerElement.textContent = `${this.turnTimeLeft}s`;
|
||
|
||
// Add warning/critical classes based on time left
|
||
timerElement.classList.remove('warning', 'critical');
|
||
if (this.turnTimeLeft <= 5) {
|
||
timerElement.classList.add('critical');
|
||
} else if (this.turnTimeLeft <= 10) {
|
||
timerElement.classList.add('warning');
|
||
}
|
||
}
|
||
|
||
updateTurnTimerStatus(status) {
|
||
const timerElement = document.getElementById('turn-timer');
|
||
if (!timerElement) return;
|
||
|
||
if (status === 'waiting') {
|
||
timerElement.classList.add('waiting');
|
||
const timeString = `${this.turnTimeLeft}s`;
|
||
timerElement.textContent = `⏸️ ${timeString}`;
|
||
} else if (status === 'active') {
|
||
timerElement.classList.remove('waiting');
|
||
this.updateTurnTimerDisplay();
|
||
}
|
||
}
|
||
|
||
handleTurnTimeout() {
|
||
this.stopTurnTimer();
|
||
this.playerTurnActive = false;
|
||
|
||
// Clear any flipped cards
|
||
if (this.flippedCards.length > 0) {
|
||
this.flippedCards.forEach(item => {
|
||
item.element.classList.remove('flipped');
|
||
});
|
||
this.flippedCards = [];
|
||
this.clearHighlights();
|
||
}
|
||
|
||
// Switch to next player
|
||
setTimeout(() => {
|
||
this.switchPlayer();
|
||
this.playerTurnActive = true;
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
// Function to handle print requests by opening PDF in print dialog
|
||
function handlePrintRequest() {
|
||
console.log('Print request intercepted!');
|
||
// Get the companion PDF URL
|
||
const pdfUrl = window.location.href.replace(/\.html$/, '.pdf');
|
||
|
||
// Create a modal overlay explaining the situation
|
||
const modal = document.createElement('div');
|
||
modal.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
z-index: 10000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
`;
|
||
|
||
const content = document.createElement('div');
|
||
content.style.cssText = `
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 30px;
|
||
max-width: 500px;
|
||
text-align: center;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
`;
|
||
|
||
const title = document.createElement('h3');
|
||
title.textContent = 'Print High-Quality Flashcards';
|
||
title.style.margin = '0 0 20px 0';
|
||
title.style.color = '#333';
|
||
|
||
const explanation = document.createElement('p');
|
||
explanation.innerHTML = 'The web version is not optimized for printing.<br>For best results, download the PDF version which includes:<br><br>• Proper card sizing and layout<br>• Cut marks for easy trimming<br>• Registration marks for alignment';
|
||
explanation.style.textAlign = 'left';
|
||
explanation.style.margin = '0 0 25px 0';
|
||
explanation.style.lineHeight = '1.6';
|
||
explanation.style.color = '#555';
|
||
|
||
const buttonContainer = document.createElement('div');
|
||
buttonContainer.style.cssText = 'display: flex; gap: 15px;';
|
||
|
||
const downloadButton = document.createElement('button');
|
||
downloadButton.textContent = '📄 Open PDF';
|
||
downloadButton.style.cssText = `
|
||
padding: 12px 24px;
|
||
background: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
`;
|
||
|
||
const cancelButton = document.createElement('button');
|
||
cancelButton.textContent = 'Cancel';
|
||
cancelButton.style.cssText = `
|
||
padding: 12px 24px;
|
||
background: #6c757d;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
`;
|
||
|
||
// Download button handler
|
||
downloadButton.onclick = function() {
|
||
showNotification('📄 Opening PDF in new tab...');
|
||
window.open(pdfUrl, '_blank');
|
||
modal.remove();
|
||
};
|
||
|
||
// Cancel button handler
|
||
cancelButton.onclick = function() {
|
||
modal.remove();
|
||
};
|
||
|
||
// Close on backdrop click
|
||
modal.onclick = function(e) {
|
||
if (e.target === modal) {
|
||
modal.remove();
|
||
}
|
||
};
|
||
|
||
// Prevent clicks inside content from closing modal
|
||
content.onclick = function(e) {
|
||
e.stopPropagation();
|
||
};
|
||
|
||
// Assemble the modal
|
||
buttonContainer.appendChild(downloadButton);
|
||
buttonContainer.appendChild(cancelButton);
|
||
content.appendChild(title);
|
||
content.appendChild(explanation);
|
||
content.appendChild(buttonContainer);
|
||
modal.appendChild(content);
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
// Fallback function to download PDF
|
||
function downloadPDF(pdfUrl) {
|
||
const link = document.createElement('a');
|
||
link.href = pdfUrl;
|
||
link.download = pdfUrl.split('/').pop();
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
}
|
||
|
||
// Function to show brief notifications
|
||
function showNotification(message) {
|
||
const notification = document.createElement('div');
|
||
notification.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: #28a745;
|
||
color: white;
|
||
padding: 15px 20px;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
z-index: 10000;
|
||
animation: slideInFromRight 0.3s ease-out;
|
||
`;
|
||
notification.textContent = message;
|
||
|
||
// Add animation styles
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
@keyframes slideInFromRight {
|
||
from { transform: translateX(100%); opacity: 0; }
|
||
to { transform: translateX(0); opacity: 1; }
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
document.body.appendChild(notification);
|
||
|
||
// Auto-remove after 3 seconds
|
||
setTimeout(() => {
|
||
notification.style.animation = 'slideInFromRight 0.3s ease-out reverse';
|
||
setTimeout(() => {
|
||
if (notification.parentNode) {
|
||
document.body.removeChild(notification);
|
||
}
|
||
if (style.parentNode) {
|
||
document.head.removeChild(style);
|
||
}
|
||
}, 300);
|
||
}, 3000);
|
||
}
|
||
|
||
// Section Navigation Manager
|
||
class SectionNavigator {
|
||
constructor() {
|
||
this.currentSection = 'introduction';
|
||
this.setupEventListeners();
|
||
}
|
||
|
||
setupEventListeners() {
|
||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
const section = e.target.getAttribute('data-section');
|
||
this.showSection(section);
|
||
});
|
||
});
|
||
}
|
||
|
||
showSection(sectionId) {
|
||
// Hide all sections
|
||
document.querySelectorAll('.page-section').forEach(section => {
|
||
section.classList.remove('active');
|
||
});
|
||
|
||
// Remove active from all nav buttons
|
||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
|
||
// Show selected section
|
||
const targetSection = document.getElementById(sectionId);
|
||
if (targetSection) {
|
||
targetSection.classList.add('active');
|
||
}
|
||
|
||
// Activate corresponding nav button
|
||
const targetBtn = document.querySelector(`[data-section="${sectionId}"]`);
|
||
if (targetBtn) {
|
||
targetBtn.classList.add('active');
|
||
}
|
||
|
||
this.currentSection = sectionId;
|
||
|
||
// Scroll to top when switching sections
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
}
|
||
|
||
// Initialize quiz and sorting when DOM is loaded
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
new ModalManager();
|
||
new SectionNavigator();
|
||
new SorobanQuiz();
|
||
new SortingChallenge();
|
||
new MatchingChallenge();
|
||
|
||
// Intercept print attempts and download PDF instead
|
||
window.addEventListener('beforeprint', (e) => {
|
||
e.preventDefault();
|
||
handlePrintRequest();
|
||
});
|
||
|
||
// Also intercept Ctrl+P / Cmd+P
|
||
document.addEventListener('keydown', (e) => {
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
|
||
e.preventDefault();
|
||
handlePrintRequest();
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |