From 204375dd39ab508eeebb027eb21c39a2b9f418bf Mon Sep 17 00:00:00 2001 From: Sebastian Molenda Date: Sun, 24 May 2026 14:37:20 +0200 Subject: [PATCH] Refactor z nowa templatka --- .gitignore | 2 + QuizzyTemplate/App | 1 + QuizzyTemplate/Doc/app.css | 410 +++++++++++ QuizzyTemplate/Doc/index.html | 397 +++++++++++ app.js | 325 ++++----- czytanie.html | 57 +- czytanie.js | 17 +- dyktando.html | 76 +- dyktando.js | 28 +- dzielenie.html | 159 +++-- dzielenie.js | 50 +- index.html | 78 +- mnozenie.html | 159 +++-- mnozenie.js | 51 +- styles.css | 1259 +++++++++++++++++++++++++++++---- testy.html | 257 +++---- 16 files changed, 2586 insertions(+), 740 deletions(-) create mode 160000 QuizzyTemplate/App create mode 100644 QuizzyTemplate/Doc/app.css create mode 100644 QuizzyTemplate/Doc/index.html diff --git a/.gitignore b/.gitignore index e43b0f9..b5b62e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +./QuizzyTemplate +./QuizzyTemplate/* diff --git a/QuizzyTemplate/App b/QuizzyTemplate/App new file mode 160000 index 0000000..149f214 --- /dev/null +++ b/QuizzyTemplate/App @@ -0,0 +1 @@ +Subproject commit 149f214357b2a991e93bab6d418c91479a78e0ac diff --git a/QuizzyTemplate/Doc/app.css b/QuizzyTemplate/Doc/app.css new file mode 100644 index 0000000..44d85a1 --- /dev/null +++ b/QuizzyTemplate/Doc/app.css @@ -0,0 +1,410 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + +:root { + --primary: #6366F1; + --primary-dark: #4F46E5; + --secondary: #8B5CF6; + --accent: #FFD166; + --text: #334155; + --text-light: #64748B; + --bg: #F8FAFC; + --bg-card: #FFFFFF; + --border: #E2E8F0; + --code-bg: #F1F5F9; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', sans-serif; + background-color: var(--bg); + color: var(--text); + line-height: 1.6; + padding: 0; + margin: 0; +} + +.container { + max-width: 1100px; + margin: 0 auto; + padding: 0 20px; +} + +header { + background: linear-gradient(135deg, var(--primary), var(--secondary)); + color: white; + padding: 60px 0 80px; + position: relative; +} + +header::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 4px; + background: var(--accent); +} + +.logo { + font-size: 24px; + font-weight: 700; + display: flex; + align-items: center; + margin-bottom: 20px; +} + +.logo-icon { + margin-right: 10px; + font-size: 28px; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.3; + margin-bottom: 20px; + color: var(--text); +} + +h1 { + font-size: 2.5em; + margin-bottom: 15px; + color: white; +} + +h2 { + font-size: 1.8em; + padding-bottom: 10px; + border-bottom: 1px solid var(--border); + margin-top: 50px; +} + +h3 { + font-size: 1.4em; + margin-top: 30px; +} + +h4 { + font-size: 1.2em; + margin-top: 20px; +} + +p { + margin-bottom: 16px; +} + +a { + color: var(--primary); + text-decoration: none; + transition: color 0.2s ease; +} + +a:hover { + color: var(--primary-dark); + text-decoration: underline; +} + +.header-subtitle { + font-size: 1.2em; + color: rgba(255, 255, 255, 0.9); + max-width: 700px; +} + +.content { + padding: 60px 0; + position: relative; +} + +.toc { + background-color: var(--bg-card); + border-radius: 10px; + padding: 25px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05); + margin-bottom: 40px; +} + +.toc-title { + font-size: 1.2em; + font-weight: 600; + margin-bottom: 15px; + color: var(--text); + display: flex; + align-items: center; +} + +.toc-title svg { + margin-right: 10px; +} + +.toc-list { + list-style-type: none; + columns: 2; + column-gap: 30px; +} + +.toc-list li { + margin-bottom: 10px; + break-inside: avoid; +} + +.toc-list a { + color: var(--text-light); + transition: color 0.2s; + display: flex; + align-items: center; +} + +.toc-list a:hover { + color: var(--primary); +} + +.toc-list a::before { + content: "•"; + margin-right: 8px; + color: var(--primary); + font-weight: bold; +} + +.section { + margin-bottom: 60px; +} + +ul, ol { + margin-left: 20px; + margin-bottom: 20px; +} + +li { + margin-bottom: 8px; +} + +code { + font-family: 'Monaco', 'Consolas', monospace; + background-color: var(--code-bg); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.9em; +} + +pre { + background-color: var(--code-bg); + padding: 15px; + border-radius: 8px; + overflow-x: auto; + margin-bottom: 20px; + border-left: 4px solid var(--primary); +} + +pre code { + background: none; + padding: 0; + font-size: 0.9em; +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.feature-card { + background: var(--bg-card); + border-radius: 8px; + padding: 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.feature-card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +} + +.feature-icon { + font-size: 24px; + margin-bottom: 12px; + color: var(--primary); +} + +.note { + background-color: rgba(99, 102, 241, 0.1); + border-left: 4px solid var(--primary); + padding: 15px; + border-radius: 0 8px 8px 0; + margin-bottom: 20px; +} + +.warning { + background-color: rgba(255, 209, 102, 0.2); + border-left: 4px solid var(--accent); + padding: 15px; + border-radius: 0 8px 8px 0; + margin-bottom: 20px; +} + +.project-structure { + font-family: 'Monaco', 'Consolas', monospace; + background-color: var(--code-bg); + padding: 20px; + border-radius: 8px; + margin-bottom: 20px; + white-space: pre; + overflow-x: auto; + line-height: 1.4; + font-size: 0.85em; +} + +.code-filename { + color: var(--primary); + font-weight: 600; + margin-bottom: 5px; + font-size: 0.9em; +} + +.steps { + list-style-type: none; + counter-reset: step-counter; + margin-left: 0; +} + +.steps li { + counter-increment: step-counter; + margin-bottom: 15px; + position: relative; + padding-left: 45px; +} + +.steps li::before { + content: counter(step-counter); + background-color: var(--primary); + color: white; + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + left: 0; + top: -1px; + font-weight: 600; + font-size: 0.9em; +} + +.table-container { + overflow-x: auto; + margin-bottom: 20px; +} + +table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +th, td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid var(--border); +} + +th { + background-color: var(--code-bg); + font-weight: 600; +} + +tr:hover { + background-color: rgba(0, 0, 0, 0.01); +} + +.badge { + display: inline-block; + background-color: var(--primary); + color: white; + border-radius: 20px; + padding: 3px 10px; + font-size: 0.75em; + font-weight: 600; + margin-right: 5px; +} + +.badge.secondary { + background-color: var(--secondary); +} + +.badge.accent { + background-color: var(--accent); + color: var(--text); +} + +.screenshot { + max-width: 100%; + border-radius: 8px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; +} + +footer { + background-color: var(--text); + color: white; + padding: 30px 0; + text-align: center; + margin-top: 100px; +} + +.back-to-top { + position: fixed; + bottom: 30px; + right: 30px; + background-color: var(--primary); + color: white; + width: 50px; + height: 50px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.back-to-top.visible { + opacity: 1; + visibility: visible; +} + +.back-to-top:hover { + background-color: var(--primary-dark); + transform: translateY(-3px); +} + +@media (max-width: 768px) { + h1 { + font-size: 2em; + } + + h2 { + font-size: 1.5em; + } + + .toc-list { + columns: 1; + } + + .container { + padding: 0 15px; + } + + .feature-grid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/QuizzyTemplate/Doc/index.html b/QuizzyTemplate/Doc/index.html new file mode 100644 index 0000000..2629c61 --- /dev/null +++ b/QuizzyTemplate/Doc/index.html @@ -0,0 +1,397 @@ + + + + + + QuizzyMind App Documentation + + + +
+
+ +

QuizzyMind App Documentation

+

A comprehensive guide to installing, configuring, and customizing your React Native Expo quiz application.

+
+
+ +
+
+ + +
+

Introduction

+ +

QuizzyMind is a fully functional quiz application built with React Native and Expo Router. This documentation will guide you through setting up and customizing the application to meet your specific needs.

+ +

Key Features

+ +
+
+
🧠
+

Multiple Quiz Categories

+

Extensive question bank with various categories

+
+ +
+
🏆
+

Real-time Leaderboards

+

Compete with friends and track progress

+
+ +
+
🎨
+

Modern UI

+

Clean design with smooth animations

+
+ +
+
🧭
+

Expo Router

+

Optimized navigation experience

+
+ +
+
📱
+

Cross-platform

+

Works on both iOS and Android

+
+
+
+ +
+

Requirements

+ +

Before you begin, ensure you have the following installed:

+ +
    +
  • Node.js (v14.0.0 or newer)
  • +
  • npm (v6.0.0 or newer) or Yarn (v1.22.0 or newer)
  • +
  • Expo CLI (npm install -g expo-cli)
  • +
  • Android Studio (for Android development)
  • +
  • Xcode (for iOS development, macOS only)
  • +
  • Git
  • +
+ +
+

Note: You can verify your Node.js and npm versions by running node -v and npm -v in your terminal.

+
+
+ +
+

Installation

+ +

Follow these steps to get the app up and running:

+ +
    +
  1. + Clone or extract the project files +
    # If you downloaded as a zip, extract it
    +cd QuizzyMind-app
    +
  2. + +
  3. + Install dependencies +
    npm install
    +# or with yarn
    +yarn install
    +
  4. + +
  5. + Start the development server +
    npx expo start or npm start
    +
  6. + +
  7. + Run on device/simulator +
      +
    • Press i to open in iOS simulator
    • +
    • Press a to open in Android emulator
    • +
    • Scan QR code with Expo Go app on physical device
    • +
    +
  8. +
+ +
+

Tip: If you encounter any issues during installation, refer to the Troubleshooting section.

+
+
+ +
+

Project Structure

+ +
QuizzyMind-app/ +├── app/ # Main application code using Expo Router +│ ├── (pages)/ # All screens inside this folder +│ ├────────── _layout.js # Root layout component +│ ├────────── CategoryScreen.jsx # Category Screen +│ ├────────── QuizScreen.jsx # QuizScreen Screen +│ ├────────── ResultScreen.jsx # ResultScreen Screen +│ ├── _layout.js # Root layout component +│ └── index.js # Home screen +├── assets/ # Static assets like images, fonts +├── components/ # Reusable UI components +│ ├── common/ # Shared components +│ ├── quiz/ # Quiz-related components +│ └── ui/ # UI elements (buttons, cards, etc.) +├── constants/ # App constants and theme settings +├── contexts/ # React contexts for state management +├── hooks/ # Custom React hooks +├── services/ # API services and external integrations +├── utils/ # Utility functions +├── app.json # Expo configuration +├── babel.config.js # Babel configuration +├── package.json # Dependencies and scripts +└── README.md # Basic readme
+
+ +
+

Key Features

+ +

Quiz Engine

+ +

The quiz system supports:

+
    +
  • Multiple quiz categories
  • +
  • Various question types (multiple choice, true/false)
  • +
  • Timed quizzes
  • +
  • Score tracking and history
  • +
  • Difficulty levels
  • +
+ +

User Interface

+ +

The UI is built with:

+
    +
  • Custom components for consistent design
  • +
  • Smooth animations and transitions
  • +
  • Dark/light mode support
  • +
  • Responsive layouts for different device sizes
  • +
+
+ +
+

Customization Guide

+ +

Theme Customization

+ +

Edit the theme variables in constants/Theme.js:

+ +
constants/Theme.js
+
// Example theme customization
+export const COLORS = {
+  primary: '#6366F1',
+  secondary: '#8B5CF6',
+  accent: '#FFD166',
+  // Add your custom colors
+};
+
+export const FONTS = {
+  // Customize font families
+};
+
+export const SIZES = {
+  // Customize spacing and sizes
+};
+ +

Content Customization

+ +

Quiz questions are stored in data/quizData.js. Modify this file to add your own questions:

+ +
data/quizData.js
+
export const QUIZ_CATEGORIES = [
+  {
+    id: 'general',
+    title: 'General Knowledge',
+    icon: 'brain',
+    questions: [
+      {
+        id: 'q1',
+        question: 'What is the capital of France?',
+        options: ['London', 'Berlin', 'Paris', 'Madrid'],
+        correctAnswer: 'Paris',
+        difficulty: 'easy'
+      },
+      // Add more questions here
+    ]
+  },
+  // Add more categories
+];
+ +

Adding New Screens

+ +

To add a new screen with Expo Router:

+ +
    +
  1. Create a new file in the app directory, e.g., app/newScreen.js
  2. +
  3. Define your screen component:
  4. +
+ +
app/newScreen.js
+
import { View, Text, StyleSheet } from 'react-native';
+import { useRouter } from 'expo-router';
+
+export default function NewScreen() {
+  const router = useRouter();
+  
+  return (
+    <View style={styles.container}>
+      <Text>New Screen Content</Text>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+});
+
+ + +
+

Deployment

+ +

Publishing to Expo

+ +
    +
  1. Create an Expo account at expo.dev
  2. +
  3. Configure your app in app.json: +
      +
    • Update name, slug, and owner
    • +
    • Set appropriate version numbers
    • +
    • Configure splash screen and icons
    • +
    +
  4. +
  5. Build and publish: +
    expo build:android    # For Android APK/AAB
    +expo build:ios        # For iOS IPA
    +expo publish          # For Expo Go distribution
    +
  6. +
+ +

Submitting to App Stores

+ +

Android (Google Play)

+ +
    +
  1. Generate a signed AAB: +
    eas build -p android --profile production
    +
  2. +
  3. Follow Google Play Console submission guidelines
  4. +
+ +

iOS (App Store)

+ +
    +
  1. Generate a signed IPA: +
    eas build -p ios --profile production
    +
  2. +
  3. Submit through App Store Connect
  4. +
+
+ +
+

Troubleshooting

+ +

Common Issues

+ +

Metro Bundler Issues

+
# Clear cache and restart
+expo start -c
+ +

Dependency Problems

+
# Reset node_modules
+rm -rf node_modules
+npm install
+ +

Expo SDK Version Conflicts

+

Ensure all packages are compatible with your Expo SDK version in package.json.

+ +
+

Tip: Most common issues can be resolved by clearing your cache and reinstalling dependencies.

+
+
+ +
+

Support

+ +

For additional support, please contact us at support@email.com or visit our support page on TemplateMonster.

+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/app.js b/app.js index 0c5d40f..786c80c 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,4 @@ -// Minimal app logic for math training (addition & subtraction) +// Minimal app logic for math training const state = { ops: new Set(), mode: null, // 'timed' or 'training' @@ -7,13 +7,15 @@ const state = { answerBuffer: '', timerId: null, timeLeft: 60, + sessionSolved: 0, + sessionTarget: 20, settings: { timedSeconds: 60, maxResult: 40, maxOperand: 20, - sessionProblems: 20, - allowNegative: false, - allowFraction: false, + sessionProblems: 20, + allowNegative: false, + allowFraction: false, } } @@ -33,28 +35,22 @@ const keypad = document.querySelectorAll('.key') const submitBtn = document.getElementById('submit') const clearBtn = document.getElementById('clear') const backspaceBtn = document.getElementById('backspace') -const historyBtn = document.getElementById('history-btn') -const historyScreen = document.getElementById('history-screen') -const historyBack = document.getElementById('history-back') -const historyList = document.getElementById('history-list') -const clearHistoryBtn = document.getElementById('clear-history') const historyPanel = document.getElementById('history-panel') -const historyToggle = document.getElementById('history-toggle') const settingTimed = document.getElementById('setting-timed') const settingMaxResult = document.getElementById('setting-max-result') const settingMaxOperand = document.getElementById('setting-max-operand') +const settingSessionProblems = document.getElementById('setting-session-problems') const settingAllowNegative = document.getElementById('setting-allow-negative') const settingAllowFraction = document.getElementById('setting-allow-fraction') const saveSettingsBtn = document.getElementById('save-settings') const resetSettingsBtn = document.getElementById('reset-settings') -const settingsToggle = document.getElementById('settings-toggle') -const settingsPanel = document.getElementById('settings-panel') -const progressInner = document.getElementById('progress-inner') +const progressInner = document.getElementById('progress_inner') const dotBtn = document.getElementById('dot') const negateBtn = document.getElementById('negate') -const summaryPanel = document.getElementById('summary-panel') +const summaryOverlay = document.getElementById('summary-overlay') const summaryText = document.getElementById('summary-text') const summaryBack = document.getElementById('summary-back') +const statusEl = document.getElementById('status'); // load settings from localStorage function loadSettings(){ @@ -69,50 +65,43 @@ function loadSettings(){ settingTimed.value = state.settings.timedSeconds settingMaxResult.value = state.settings.maxResult settingMaxOperand.value = state.settings.maxOperand - const sess = state.settings.sessionProblems || 20 - const settingSessionProblems = document.getElementById('setting-session-problems') - if (settingSessionProblems) settingSessionProblems.value = sess - if (settingAllowNegative) settingAllowNegative.checked = !!state.settings.allowNegative - if (settingAllowFraction) settingAllowFraction.checked = !!state.settings.allowFraction + settingSessionProblems.value = state.settings.sessionProblems + settingAllowNegative.checked = !!state.settings.allowNegative + settingAllowFraction.checked = !!state.settings.allowFraction } function saveSettings(){ - const timed = parseInt(settingTimed.value,10) || state.settings.timedSeconds - const maxResult = parseInt(settingMaxResult.value,10) || state.settings.maxResult - const maxOperand = parseInt(settingMaxOperand.value,10) || state.settings.maxOperand - state.settings.timedSeconds = Math.max(5, Math.min(600, timed)) - state.settings.maxResult = Math.max(1, Math.min(999, maxResult)) - state.settings.maxOperand = Math.max(1, Math.min(999, maxOperand)) - const settingSessionProblems = document.getElementById('setting-session-problems') - if (settingSessionProblems) state.settings.sessionProblems = Math.max(1, Math.min(500, parseInt(settingSessionProblems.value,10) || state.settings.sessionProblems)) - if (settingAllowNegative) state.settings.allowNegative = !!settingAllowNegative.checked - if (settingAllowFraction) state.settings.allowFraction = !!settingAllowFraction.checked + state.settings.timedSeconds = Math.max(5, Math.min(600, parseInt(settingTimed.value,10) || 60)) + state.settings.maxResult = Math.max(1, Math.min(999, parseInt(settingMaxResult.value,10) || 40)) + state.settings.maxOperand = Math.max(1, Math.min(999, parseInt(settingMaxOperand.value,10) || 20)) + state.settings.sessionProblems = Math.max(1, Math.min(500, parseInt(settingSessionProblems.value,10) || 20)) + state.settings.allowNegative = !!settingAllowNegative.checked + state.settings.allowFraction = !!settingAllowFraction.checked try{ localStorage.setItem('matma:settings', JSON.stringify(state.settings)) }catch(e){console.warn('save failed',e)} + // visual feedback - feedbackEl.textContent = 'Ustawienia zapisane' - setTimeout(()=>feedbackEl.textContent = '',1500) + const parent = saveSettingsBtn.parentElement; + const feedback = document.createElement('span'); + feedback.textContent = 'Zapisano!'; + feedback.style.color = '#16a34a'; + feedback.style.marginLeft = '12px'; + parent.appendChild(feedback); + setTimeout(()=> parent.removeChild(feedback), 2000); } function resetSettings(){ - state.settings = {timedSeconds:60, maxResult:40, maxOperand:20, sessionProblems:20} - saveSettings() - loadSettings() + state.settings = {timedSeconds:60, maxResult:40, maxOperand:20, sessionProblems:20, allowNegative: false, allowFraction: false}; + saveSettings(); + loadSettings(); } saveSettingsBtn.addEventListener('click', saveSettings) resetSettingsBtn.addEventListener('click', resetSettings) -// settings toggle -settingsToggle.addEventListener('click', ()=>{ - const collapsed = settingsPanel.classList.toggle('collapsed') - settingsToggle.textContent = collapsed ? 'Pokaż' : 'Ukryj' -}) - loadSettings() // initial wiring opButtons.forEach(b => { - if (b.classList.contains('disabled')) return b.addEventListener('click', () => { const op = b.dataset.op if (state.ops.has(op)) { state.ops.delete(op); b.classList.remove('active') } @@ -121,14 +110,19 @@ opButtons.forEach(b => { }) modeButtons.forEach(b => b.addEventListener('click', () => { + if (state.ops.size === 0) { + const hint = document.querySelector('.hint-text'); + hint.style.color = '#dc2626'; + hint.style.fontWeight = '600'; + setTimeout(() => { + hint.style.color = ''; + hint.style.fontWeight = ''; + }, 2000); + return; + } modeButtons.forEach(x=>x.classList.remove('active')) b.classList.add('active') state.mode = b.dataset.mode - // require at least one op selected to start - if (state.ops.size === 0) { - feedbackEl.textContent = 'Wybierz co najmniej jedno działanie.' - return - } startPlay() })) @@ -149,51 +143,43 @@ function startPlay(){ feedbackEl.textContent = '' state.currentProblem = generateProblem() renderProblem() - // make problem larger for mobile clarity - problemEl.classList.add('big') - // init progress + if (state.mode === 'timed'){ progressInner.style.width = '0%' startTimer(state.settings.timedSeconds) + statusEl.textContent = 'Na czas'; + timerEl.classList.remove('hidden'); } else { progressInner.style.width = '0%' state.sessionSolved = 0 - state.sessionTarget = Math.max(1, state.settings.sessionProblems || 20) + state.sessionTarget = Math.max(1, state.settings.sessionProblems) updateProgress() - hideTimer() + timerEl.classList.add('hidden'); } } function generateProblem(){ - // choose an operation from selected ops const ops = Array.from(state.ops) const op = ops[Math.floor(Math.random()*ops.length)] - // generate operands within maxOperand and ensure result within maxResult const maxOp = Math.max(1, state.settings.maxOperand) const maxRes = Math.max(1, state.settings.maxResult) let a = 0, b = 0 - // try a few times to satisfy constraints for (let i=0;i<200;i++){ if (op === 'div'){ - // divisor b should be non-zero b = randInt(1, maxOp) if (!state.settings.allowFraction){ - // produce integer result q such that a = q * b and a <= maxOp and q <= maxRes const maxQByOp = Math.floor(maxOp / b) const maxQ = Math.min(maxRes, maxQByOp) if (maxQ < 0) continue const q = randInt(0, maxQ) a = q * b - // optional negative results if (state.settings.allowNegative && Math.random() < 0.2) a = -a return {a,b,op} } else { - // allow fractional results: generate tenths (one decimal place) const maxResultNByOp = Math.floor((10 * maxOp) / b) const maxResultN = Math.min(maxRes * 10, maxResultNByOp) if (maxResultN < 0) continue const result_n = randInt(0, maxResultN) - // ensure dividend is integer: (result_n * b) % 10 === 0 if ((result_n * b) % 10 !== 0) continue a = (result_n * b) / 10 if (state.settings.allowNegative && Math.random() < 0.2) a = -a @@ -214,12 +200,10 @@ function generateProblem(){ if (a < b) [a,b] = [b,a] if (a - b <= maxRes) return {a,b,op} } else { - // negative allowed: result absolute should be <= maxRes if (Math.abs(a - b) <= maxRes) return {a,b,op} } } } - // fallback: return simple small numbers a = Math.min(maxOp, Math.floor(maxRes/2)) b = Math.min(maxOp, 1) return {a,b,op} @@ -233,7 +217,7 @@ function renderProblem(){ const p = state.currentProblem const map = {add: '+', sub: '−', mul: '×', div: '÷'} const symbol = map[p.op] || '?' - problemEl.textContent = `${p.a} ${symbol} ${p.b} = ?` + problemEl.textContent = `${p.a} ${symbol} ${p.b}` } function submitAnswer(){ @@ -248,30 +232,40 @@ function submitAnswer(){ else if (p.op === 'div') correct = (p.b === 0) ? null : (p.a / p.b) const eps = state.settings.allowFraction ? 1e-6 : 1e-9 const isCorrect = (correct !== null) && (Math.abs(given - correct) < eps) + + feedbackEl.style.opacity = 1; if (isCorrect) { state.score += 1 - feedbackEl.textContent = '✔ dobrze' + feedbackEl.textContent = 'Dobrze!' + feedbackEl.classList.add('correct'); + feedbackEl.classList.remove('incorrect'); } else { - feedbackEl.textContent = `✖ poprawne: ${Number.isFinite(correct) ? correct : '—'}` + const correctAnswer = Number.isFinite(correct) ? parseFloat(correct.toFixed(2)) : '—'; + feedbackEl.textContent = `Poprawna odpowiedź: ${correctAnswer}` + feedbackEl.classList.add('incorrect'); + feedbackEl.classList.remove('correct'); } scoreEl.textContent = state.score - // next problem state.answerBuffer = '' answerEl.textContent = '' - // increment solved count and handle training limit + if (state.mode === 'training'){ - state.sessionSolved = (state.sessionSolved || 0) + 1 + state.sessionSolved++ updateProgress() if (state.sessionSolved >= state.sessionTarget){ - // reached limit -> show summary instead of next problem saveSessionToHistory() - showSummary() + setTimeout(showSummary, 1200); return } } - // otherwise generate next problem - state.currentProblem = generateProblem() - renderProblem() + + setTimeout(() => { + feedbackEl.style.opacity = 0; + setTimeout(() => { + state.currentProblem = generateProblem() + renderProblem() + }, 200); + }, 1200); } submitBtn.addEventListener('click', submitAnswer) @@ -318,13 +312,13 @@ backspaceBtn.addEventListener('click', ()=>{ function startTimer(seconds){ state.timeLeft = seconds timerEl.textContent = state.timeLeft - timerEl.classList.remove('hidden') + + stopTimer(); state.timerId = setInterval(()=>{ state.timeLeft -= 1 timerEl.textContent = state.timeLeft - // update progress bar for timed mode - const pct = Math.max(0, ((seconds - state.timeLeft) / seconds) * 100) - progressInner.style.width = pct + '%' + const pct = Math.max(0, (state.timeLeft / seconds) * 100) + progressInner.style.width = pct + '%' if (state.timeLeft <= 0) { stopTimer() endSession() @@ -337,58 +331,35 @@ function stopTimer(){ state.timerId = null } -function hideTimer(){ - timerEl.classList.add('hidden') -} - function endSession(){ - feedbackEl.textContent = `Koniec! Wynik: ${state.score}` saveSessionToHistory() - // fill progress - progressInner.style.width = '100%' - // return to menu after short pause (timed mode) - setTimeout(()=>{ - playScreen.classList.add('hidden') - menuScreen.classList.remove('hidden') - },2500) + showSummary(); } function showSummary(){ - // hide problem area and keypad, show summary panel - const summaryTextEl = summaryText - if (summaryTextEl) summaryTextEl.textContent = `Poprawne odpowiedzi: ${state.score}` - // hide play UI sections - const problemArea = document.querySelector('.problem-area') - const answerArea = document.querySelector('.answer-area') - if (problemArea) problemArea.classList.add('hidden') - if (answerArea) answerArea.classList.add('hidden') - if (summaryPanel) summaryPanel.classList.remove('hidden') + const total = state.mode === 'timed' ? state.score : state.sessionTarget; + const correct = state.score; + const pct = total > 0 ? Math.round((correct / total) * 100) : 0; + + if (state.mode === 'timed') { + summaryText.textContent = `Koniec czasu! Zdobyłeś ${correct} punktów.`; + } else { + summaryText.textContent = `Twój wynik: ${correct} / ${total} poprawnie (${pct}%)`; + } + summaryOverlay.classList.remove('hidden'); } -function hideSummary(){ - const problemArea = document.querySelector('.problem-area') - const answerArea = document.querySelector('.answer-area') - if (problemArea) problemArea.classList.remove('hidden') - if (answerArea) answerArea.classList.remove('hidden') - if (summaryPanel) summaryPanel.classList.add('hidden') -} - -if (summaryBack) summaryBack.addEventListener('click', ()=>{ - // close summary and go back to menu - hideSummary() - playScreen.classList.add('hidden') - menuScreen.classList.remove('hidden') -}) +summaryBack.addEventListener('click', ()=>{ + summaryOverlay.classList.add('hidden'); + playScreen.classList.add('hidden'); + menuScreen.classList.remove('hidden'); +}); function updateProgress(){ if (state.mode === 'training'){ - const solved = state.sessionSolved || 0 - const target = state.sessionTarget || 1 - const pct = Math.min(100, Math.round((solved/target)*100)) + const pct = Math.min(100, Math.round((state.sessionSolved/state.sessionTarget)*100)) progressInner.style.width = pct + '%' - // show count in status - const status = document.getElementById('status') - if (status) status.textContent = `${solved}/${target}` + statusEl.textContent = `${state.sessionSolved}/${state.sessionTarget}` } } @@ -402,69 +373,61 @@ function loadHistory(){ function saveSessionToHistory(){ const h = loadHistory() - h.unshift({ts: Date.now(), mode: state.mode, score: state.score, settings: {...state.settings}}) - // keep only last 100 - while(h.length>100) h.pop() + const sessionData = { + ts: Date.now(), + mode: state.mode, + score: state.score, + ops: Array.from(state.ops), + duration: state.mode === 'timed' ? state.settings.timedSeconds : null, + problems: state.mode === 'training' ? state.sessionTarget : null + }; + h.unshift(sessionData); + while(h.length>50) h.pop() try{ localStorage.setItem('matma:history', JSON.stringify(h)) }catch(e){ } } function renderHistory(){ const h = loadHistory() - historyList.innerHTML = '' + historyPanel.innerHTML = '' if (h.length === 0) { - historyList.innerHTML = '
Brak zapisanych sesji
' - if (historyList) historyList.innerHTML = historyList.innerHTML - return + historyPanel.innerHTML = '

Brak zapisanych sesji.

' + return } h.forEach(item => { - const d = new Date(item.ts).toLocaleString() + const d = new Date(item.ts).toLocaleString('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); + const modeText = item.mode === 'timed' ? `Na czas (${item.duration}s)` : `Trening (${item.problems} zadań)`; + const opsText = item.ops.join(', ').replace('add','+').replace('sub','-').replace('mul','×').replace('div','÷'); + const el = document.createElement('div') el.className = 'history-item' - el.innerHTML = `
${item.mode === 'timed' ? 'Na czas' : 'Trening'} — Wynik: ${item.score}
${d} • czas:${item.settings.timedSeconds}s maxRes:${item.settings.maxResult} maxOp:${item.settings.maxOperand}
` - historyList.appendChild(el) + el.innerHTML = ` +
+ Wynik: ${item.score} + ${opsText} +
+
+ ${modeText} + ${d} +
` + historyPanel.appendChild(el) }) - if (historyList) historyList.innerHTML = historyList.innerHTML } -// Toggle in-menu history panel (behaves like settings) -function toggleHistory(){ - if (!historyPanel) return - // toggle collapsed class (collapsed => hidden) - const nowCollapsed = historyPanel.classList.toggle('collapsed') - // nowCollapsed is true when panel is hidden - if (!nowCollapsed) renderHistory() - // keep main menu button label constant; header toggle shows state - historyBtn.textContent = 'Historia' - if (historyToggle) historyToggle.textContent = nowCollapsed ? 'Pokaż' : 'Ukryj' +// Toggle history panel +const historyDetails = historyPanel.closest('details'); +if (historyDetails) { + historyDetails.addEventListener('toggle', () => { + if (historyDetails.open) { + renderHistory(); + } + }); } - -// history toggle -historyToggle.addEventListener('click', ()=>{ - const collapsed = historyPanel.classList.toggle('collapsed') - historyToggle.textContent = collapsed ? 'Pokaż' : 'Ukryj' - if (!collapsed) renderHistory() - else historyList.innerHTML = '' -}) - -// historyBtn.addEventListener('click', toggleHistory) -// if (historyToggle) historyToggle.addEventListener('click', toggleHistory) - -// historyBack.addEventListener('click', ()=>{ -// historyScreen.classList.add('hidden') -// menuScreen.classList.remove('hidden') -// }) - -// clearHistoryBtn.addEventListener('click', ()=>{ -// try{ localStorage.removeItem('matma:history') }catch(e){} -// renderHistory() -// }) - -// keyboard support: allow Enter/Backspace/0-9 +// keyboard support window.addEventListener('keydown', (e)=>{ if (playScreen.classList.contains('hidden')) return if (/^[0-9]$/.test(e.key)){ - if (state.answerBuffer.length < 6) { + if (state.answerBuffer.length < 12) { state.answerBuffer += e.key answerEl.textContent = state.answerBuffer } @@ -473,32 +436,18 @@ window.addEventListener('keydown', (e)=>{ answerEl.textContent = state.answerBuffer } else if (e.key === 'Enter'){ submitAnswer() + } else if (e.key === '.' || e.key === ',') { + dotBtn.click(); } }) -// small helper to set initial focus - mobile browsers will not open keyboard for divs -document.addEventListener('touchstart', ()=>{}, {passive:true}) - -// Load commit SHA into footer (simple: prefer window.COMMIT_SHA/meta, else fetch /version.sha which contains only the hash) -document.addEventListener('DOMContentLoaded', async () => { - const el = document.getElementById('commit-sha') - if (!el) return - let sha = (window.COMMIT_SHA || '').toString().trim() - if (!sha) { - const meta = document.querySelector('meta[name="commit-sha"]') - if (meta && meta.content) sha = meta.content.trim() - } - if (!sha) { - try { - const res = await fetch('/version.sha', { cache: 'no-cache' }) - if (res.ok) { - const txt = await res.text() - const first = txt.split(/\r?\n/).find(l => l.trim().length > 0) - if (first) sha = first.trim() - } - } catch (e) { - // ignore - } - } - if (sha) el.textContent = sha.slice(0,8) -}) +// Set default active operations +document.addEventListener('DOMContentLoaded', () => { + opButtons.forEach(btn => { + const op = btn.dataset.op; + if (['add', 'sub'].includes(op)) { + btn.classList.add('active'); + state.ops.add(op); + } + }); +}); diff --git a/czytanie.html b/czytanie.html index 7310e76..95a71d8 100644 --- a/czytanie.html +++ b/czytanie.html @@ -4,50 +4,59 @@ Czytanie - + + + + -
-