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
+
+
QuizzyMind App Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Table of Contents
+
+
+
+
+
+ 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:
+
+
+
+ Clone or extract the project files
+ # If you downloaded as a zip, extract it
+cd QuizzyMind-app
+
+
+
+ Install dependencies
+ npm install
+# or with yarn
+yarn install
+
+
+
+ Start the development server
+ npx expo start or npm start
+
+
+
+ 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
+
+
+
+
+
+
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:
+
+
+ Create a new file in the app directory, e.g., app/newScreen.js
+ Define your screen component:
+
+
+ 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
+
+
+ Create an Expo account at expo.dev
+ Configure your app in app.json:
+
+ Update name, slug, and owner
+ Set appropriate version numbers
+ Configure splash screen and icons
+
+
+ Build and publish:
+ expo build:android # For Android APK/AAB
+expo build:ios # For iOS IPA
+expo publish # For Expo Go distribution
+
+
+
+ Submitting to App Stores
+
+ Android (Google Play)
+
+
+ Generate a signed AAB:
+ eas build -p android --profile production
+
+ Follow Google Play Console submission guidelines
+
+
+ iOS (App Store)
+
+
+ Generate a signed IPA:
+ eas build -p ios --profile production
+
+ Submit through App Store Connect
+
+
+
+
+ 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
-
+
+
+
+
-
-
- ← Menu
+
+
+ ← Wróć do menu
-
-
- 📖 Czytanie
+
+
-
+
-
- Własny tekst
-
- Czytaj →
-
-
-
+
+ Własny tekst
+
+ Czytaj ten tekst →
+
+
-