Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d406f10206 |
@@ -1,43 +0,0 @@
|
||||
name: Build APK
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: cimg/android:2024.01
|
||||
|
||||
steps:
|
||||
- name: Install Node.js
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Generate Gradle Wrapper
|
||||
run: gradle wrapper
|
||||
|
||||
- name: Grant execute permission
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Build APK
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-release
|
||||
path: app/build/outputs/apk/release/app-release.apk
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-release
|
||||
path: app/build/outputs/apk/release/app-release.apk
|
||||
@@ -25,6 +25,6 @@ jobs:
|
||||
server: ${{ secrets.FTP_HOST }}
|
||||
username: ${{ secrets.FTP_USER }}
|
||||
password: ${{ secrets.FTP_PASS }}
|
||||
local-dir: app/src/main/assets/
|
||||
local-dir: ./
|
||||
server-dir: /
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
#Wed May 27 00:12:10 CEST 2026
|
||||
gradle.version=8.5
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
package com.example.app
|
||||
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val webView = WebView(this)
|
||||
|
||||
webView.settings.javaScriptEnabled = true
|
||||
webView.settings.allowFileAccess = true
|
||||
webView.settings.domStorageEnabled = true
|
||||
|
||||
webView.webViewClient = WebViewClient()
|
||||
|
||||
webView.loadUrl("file:///android_asset/index.html")
|
||||
|
||||
setContentView(webView)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" packagePrefix="com.example.app" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Submodule
+1
Submodule QuizzyTemplate/App added at 149f214357
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>QuizzyMind App Documentation</title>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<span class="logo-icon">🎮</span>
|
||||
Quizzy<span style="color: var(--accent);">Mind</span>
|
||||
</div>
|
||||
<h1>QuizzyMind App Documentation</h1>
|
||||
<p class="header-subtitle">A comprehensive guide to installing, configuring, and customizing your React Native Expo quiz application.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div class="toc" id="table-of-contents">
|
||||
<h3 class="toc-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
Table of Contents
|
||||
</h3>
|
||||
<ul class="toc-list">
|
||||
<li><a href="#introduction">Introduction</a></li>
|
||||
<li><a href="#requirements">Requirements</a></li>
|
||||
<li><a href="#installation">Installation</a></li>
|
||||
<li><a href="#project-structure">Project Structure</a></li>
|
||||
<li><a href="#configuration">Configuration</a></li>
|
||||
<li><a href="#key-features">Key Features</a></li>
|
||||
<li><a href="#customization-guide">Customization Guide</a></li>
|
||||
<li><a href="#api-integration">API Integration</a></li>
|
||||
<li><a href="#deployment">Deployment</a></li>
|
||||
<li><a href="#troubleshooting">Troubleshooting</a></li>
|
||||
<li><a href="#support">Support</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<section class="section" id="introduction">
|
||||
<h2>Introduction</h2>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<h3>Key Features</h3>
|
||||
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🧠</div>
|
||||
<h4>Multiple Quiz Categories</h4>
|
||||
<p>Extensive question bank with various categories</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🏆</div>
|
||||
<h4>Real-time Leaderboards</h4>
|
||||
<p>Compete with friends and track progress</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🎨</div>
|
||||
<h4>Modern UI</h4>
|
||||
<p>Clean design with smooth animations</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🧭</div>
|
||||
<h4>Expo Router</h4>
|
||||
<p>Optimized navigation experience</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📱</div>
|
||||
<h4>Cross-platform</h4>
|
||||
<p>Works on both iOS and Android</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="requirements">
|
||||
<h2>Requirements</h2>
|
||||
|
||||
<p>Before you begin, ensure you have the following installed:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Node.js</strong> (v14.0.0 or newer)</li>
|
||||
<li><strong>npm</strong> (v6.0.0 or newer) or <strong>Yarn</strong> (v1.22.0 or newer)</li>
|
||||
<li><strong>Expo CLI</strong> (<code>npm install -g expo-cli</code>)</li>
|
||||
<li><strong>Android Studio</strong> (for Android development)</li>
|
||||
<li><strong>Xcode</strong> (for iOS development, macOS only)</li>
|
||||
<li><strong>Git</strong></li>
|
||||
</ul>
|
||||
|
||||
<div class="note">
|
||||
<p><strong>Note:</strong> You can verify your Node.js and npm versions by running <code>node -v</code> and <code>npm -v</code> in your terminal.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="installation">
|
||||
<h2>Installation</h2>
|
||||
|
||||
<p>Follow these steps to get the app up and running:</p>
|
||||
|
||||
<ol class="steps">
|
||||
<li>
|
||||
<strong>Clone or extract the project files</strong>
|
||||
<pre><code># If you downloaded as a zip, extract it
|
||||
cd QuizzyMind-app</code></pre>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Install dependencies</strong>
|
||||
<pre><code>npm install
|
||||
# or with yarn
|
||||
yarn install</code></pre>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Start the development server</strong>
|
||||
<pre><code>npx expo start or npm start</code></pre>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Run on device/simulator</strong>
|
||||
<ul>
|
||||
<li>Press <code>i</code> to open in iOS simulator</li>
|
||||
<li>Press <code>a</code> to open in Android emulator</li>
|
||||
<li>Scan QR code with Expo Go app on physical device</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="note">
|
||||
<p><strong>Tip:</strong> If you encounter any issues during installation, refer to the <a href="#troubleshooting">Troubleshooting</a> section.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="project-structure">
|
||||
<h2>Project Structure</h2>
|
||||
|
||||
<div class="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</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="key-features">
|
||||
<h2>Key Features</h2>
|
||||
|
||||
<h3>Quiz Engine</h3>
|
||||
|
||||
<p>The quiz system supports:</p>
|
||||
<ul>
|
||||
<li>Multiple quiz categories</li>
|
||||
<li>Various question types (multiple choice, true/false)</li>
|
||||
<li>Timed quizzes</li>
|
||||
<li>Score tracking and history</li>
|
||||
<li>Difficulty levels</li>
|
||||
</ul>
|
||||
|
||||
<h3>User Interface</h3>
|
||||
|
||||
<p>The UI is built with:</p>
|
||||
<ul>
|
||||
<li>Custom components for consistent design</li>
|
||||
<li>Smooth animations and transitions</li>
|
||||
<li>Dark/light mode support</li>
|
||||
<li>Responsive layouts for different device sizes</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="section" id="customization-guide">
|
||||
<h2>Customization Guide</h2>
|
||||
|
||||
<h3>Theme Customization</h3>
|
||||
|
||||
<p>Edit the theme variables in <code>constants/Theme.js</code>:</p>
|
||||
|
||||
<div class="code-filename">constants/Theme.js</div>
|
||||
<pre><code>// 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
|
||||
};</code></pre>
|
||||
|
||||
<h3>Content Customization</h3>
|
||||
|
||||
<p>Quiz questions are stored in <code>data/quizData.js</code>. Modify this file to add your own questions:</p>
|
||||
|
||||
<div class="code-filename">data/quizData.js</div>
|
||||
<pre><code>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
|
||||
];</code></pre>
|
||||
|
||||
<h3>Adding New Screens</h3>
|
||||
|
||||
<p>To add a new screen with Expo Router:</p>
|
||||
|
||||
<ol>
|
||||
<li>Create a new file in the <code>app</code> directory, e.g., <code>app/newScreen.js</code></li>
|
||||
<li>Define your screen component:</li>
|
||||
</ol>
|
||||
|
||||
<div class="code-filename">app/newScreen.js</div>
|
||||
<pre><code>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',
|
||||
},
|
||||
});</code></pre>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="section" id="deployment">
|
||||
<h2>Deployment</h2>
|
||||
|
||||
<h3>Publishing to Expo</h3>
|
||||
|
||||
<ol>
|
||||
<li>Create an Expo account at <a href="https://expo.dev" target="_blank">expo.dev</a></li>
|
||||
<li>Configure your app in <code>app.json</code>:
|
||||
<ul>
|
||||
<li>Update <code>name</code>, <code>slug</code>, and <code>owner</code></li>
|
||||
<li>Set appropriate version numbers</li>
|
||||
<li>Configure splash screen and icons</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Build and publish:
|
||||
<pre><code>expo build:android # For Android APK/AAB
|
||||
expo build:ios # For iOS IPA
|
||||
expo publish # For Expo Go distribution</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>Submitting to App Stores</h3>
|
||||
|
||||
<h4>Android (Google Play)</h4>
|
||||
|
||||
<ol>
|
||||
<li>Generate a signed AAB:
|
||||
<pre><code>eas build -p android --profile production</code></pre>
|
||||
</li>
|
||||
<li>Follow Google Play Console submission guidelines</li>
|
||||
</ol>
|
||||
|
||||
<h4>iOS (App Store)</h4>
|
||||
|
||||
<ol>
|
||||
<li>Generate a signed IPA:
|
||||
<pre><code>eas build -p ios --profile production</code></pre>
|
||||
</li>
|
||||
<li>Submit through App Store Connect</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="section" id="troubleshooting">
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<h3>Common Issues</h3>
|
||||
|
||||
<h4>Metro Bundler Issues</h4>
|
||||
<pre><code># Clear cache and restart
|
||||
expo start -c</code></pre>
|
||||
|
||||
<h4>Dependency Problems</h4>
|
||||
<pre><code># Reset node_modules
|
||||
rm -rf node_modules
|
||||
npm install</code></pre>
|
||||
|
||||
<h4>Expo SDK Version Conflicts</h4>
|
||||
<p>Ensure all packages are compatible with your Expo SDK version in <code>package.json</code>.</p>
|
||||
|
||||
<div class="note">
|
||||
<p><strong>Tip:</strong> Most common issues can be resolved by clearing your cache and reinstalling dependencies.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="support">
|
||||
<h2>Support</h2>
|
||||
|
||||
<p>For additional support, please contact us at <a href="mailto:support@email.com">support@email.com</a> or visit our support page on TemplateMonster.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2025 QuizzyMind App | All Rights Reserved</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<a href="#" class="back-to-top" id="backToTop">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 15l-6-6-6 6"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<script>
|
||||
// Back to top button functionality
|
||||
const backToTopButton = document.getElementById('backToTop');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.pageYOffset > 300) {
|
||||
backToTopButton.classList.add('visible');
|
||||
} else {
|
||||
backToTopButton.classList.remove('visible');
|
||||
}
|
||||
});
|
||||
|
||||
backToTopButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
// Smooth scrolling for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.getAttribute('href') === '#') return;
|
||||
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "QuizzyTemplate",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ const statusEl = document.getElementById('status');
|
||||
// load settings from localStorage
|
||||
function loadSettings(){
|
||||
try{
|
||||
let progressInner = null
|
||||
const raw = localStorage.getItem('matma:settings')
|
||||
if (raw) {
|
||||
const s = JSON.parse(raw)
|
||||
state.settings = Object.assign(state.settings, s)
|
||||
@@ -1,38 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'com.example.app'
|
||||
compileSdk 33
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.app"
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,2 +0,0 @@
|
||||
#- File Locator -
|
||||
listingFile=../../../outputs/apk/release/output-metadata.json
|
||||
@@ -1,2 +0,0 @@
|
||||
appMetadataVersion=1.1
|
||||
androidGradlePluginVersion=8.2.2
|
||||
@@ -1 +0,0 @@
|
||||
<footer class="app-footer">Version: <span id="commit-sha">(loading)</span></footer>
|
||||
@@ -1,5 +0,0 @@
|
||||
<!-- Uniwersalny nagłówek aplikacji/substrony -->
|
||||
<header class="subpage-header">
|
||||
<h1 class="subpage-title">{TITLE}</h1>
|
||||
<p class="subpage-subtitle">{SUBTITLE}</p>
|
||||
</header>
|
||||
@@ -1,4 +0,0 @@
|
||||
<!-- Uniwersalny pasek postępu -->
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-inner" id="{PROGRESS_ID}"></div>
|
||||
</div>
|
||||
@@ -1,27 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('../../fonts/Inter-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('../../fonts/Inter-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('../../fonts/Inter-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('../../fonts/Inter-ExtraBold.ttf') format('truetype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
}
|
||||
body, html {
|
||||
font-family: 'Inter', Arial, sans-serif;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,116 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>Nauka Dzielenia</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=20260524" />
|
||||
<link rel="stylesheet" href="css/fonts/inter.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- ── SELECT SCREEN ── -->
|
||||
<main id="select-screen">
|
||||
<nav class="subpage-nav">
|
||||
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
|
||||
</nav>
|
||||
<div class="screen-content">
|
||||
<div id="header-placeholder"></div>
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">Wybór dzielnika</h2>
|
||||
<div class="table-grid" id="table-grid">
|
||||
<button class="table-btn" data-val="1">1</button>
|
||||
<button class="table-btn" data-val="2">2</button>
|
||||
<button class="table-btn" data-val="3">3</button>
|
||||
<button class="table-btn" data-val="4">4</button>
|
||||
<button class="table-btn" data-val="5">5</button>
|
||||
<button class="table-btn" data-val="6">6</button>
|
||||
<button class="table-btn" data-val="7">7</button>
|
||||
<button class="table-btn" data-val="8">8</button>
|
||||
<button class="table-btn" data-val="9">9</button>
|
||||
<button class="table-btn" data-val="10">10</button>
|
||||
<button class="table-btn all" data-val="0">Wszystkie</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="content-panel">
|
||||
<label class="settings-label">
|
||||
Liczba zadań w serii:
|
||||
<input id="total-input" type="number" min="5" max="100" value="20" class="settings-input">
|
||||
</label>
|
||||
</section>
|
||||
<button class="action-btn start-btn" id="start-btn">Rozpocznij ćwiczenie →</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── PLAY SCREEN ── -->
|
||||
<main class="play-screen-container hidden" id="play-screen">
|
||||
<header class="read-header">
|
||||
<button id="back-btn" class="read-nav-btn">← Zmień</button>
|
||||
<span class="read-title-text">Dzielenie</span>
|
||||
<div class="progress-container">
|
||||
<span id="progress-label" class="progress-text">0/20</span>
|
||||
</div>
|
||||
</header>
|
||||
<div id="progressbar-placeholder"></div>
|
||||
|
||||
<div class="play-body">
|
||||
<div class="problem-display">
|
||||
<div id="problem" class="problem-text">?</div>
|
||||
<div id="feedback" class="feedback-text"></div>
|
||||
</div>
|
||||
|
||||
<div class="keypad-container">
|
||||
<div id="answer" class="answer-display"></div>
|
||||
<div class="keypad">
|
||||
<div class="key-row">
|
||||
<button class="key">1</button><button class="key">2</button><button class="key">3</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">4</button><button class="key">5</button><button class="key">6</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">7</button><button class="key">8</button><button class="key">9</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key key-special" id="clear">C</button>
|
||||
<button class="key">0</button>
|
||||
<button class="key key-special" id="backspace">⌫</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="submit-btn" id="submit">Sprawdź</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── SUMMARY SCREEN ── -->
|
||||
<main class="screen-content hidden" id="summary-screen" style="margin-top: 24px;">
|
||||
<div class="summary-view">
|
||||
<div style="font-size: 64px; text-align: center;">🎉</div>
|
||||
<h2 style="text-align: center; font-size: 28px; margin: 16px 0;">Koniec ćwiczenia!</h2>
|
||||
<p id="summary-text" class="summary-score-text"></p>
|
||||
<button class="action-btn summary-back-btn" id="again-btn">Jeszcze raz</button>
|
||||
<button class="action-btn summary-back-btn secondary" id="change-btn">Zmień dzielnik</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="footer-placeholder"></div>
|
||||
<script src="js/nav.js?v=20260521"></script>
|
||||
<script src="js/dzielenie.js?v=20260524"></script>
|
||||
<script src="js/components.js"></script>
|
||||
<script>
|
||||
loadComponent('components/header.html', {
|
||||
'{TITLE}': '÷ Nauka Dzielenia',
|
||||
'{SUBTITLE}': 'Wybierz dzielnik, przez który chcesz dzielić.'
|
||||
}).then(html => {
|
||||
document.getElementById('header-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/progress-bar.html', {'{PROGRESS_ID}': 'progress-inner'}).then(html => {
|
||||
document.getElementById('progressbar-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/footer.html').then(html => {
|
||||
document.getElementById('footer-placeholder').outerHTML = html;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,63 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>Edu — Główne Menu</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=20260524" />
|
||||
<link rel="stylesheet" href="css/fonts/inter.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<header class="app-header">
|
||||
<h1 class="app-title">Wybierz kategorię</h1>
|
||||
<p class="app-subtitle">i rozpocznij naukę</p>
|
||||
</header>
|
||||
|
||||
<main class="category-grid">
|
||||
<a href="czytanie.html" class="category-card" style="--grad-start: #3B82F6; --grad-end: #1D4ED8;">
|
||||
<div class="card-icon">📖</div>
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">Czytanie</h2>
|
||||
<p class="card-desc">Ćwicz płynność czytania</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="dyktando.html" class="category-card" style="--grad-start: #F59E0B; --grad-end: #D97706;">
|
||||
<div class="card-icon">✏️</div>
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">Dyktando</h2>
|
||||
<p class="card-desc">Sprawdź swoją ortografię</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="mnozenie.html" class="category-card" style="--grad-start: #10B981; --grad-end: #059669;">
|
||||
<div class="card-icon">×</div>
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">Mnożenie</h2>
|
||||
<p class="card-desc">Tabliczka mnożenia</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="dzielenie.html" class="category-card" style="--grad-start: #8B5CF6; --grad-end: #7C3AED;">
|
||||
<div class="card-icon">÷</div>
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">Dzielenie</h2>
|
||||
<p class="card-desc">Powtórka z dzielenia</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="testy.html" class="category-card" style="--grad-start: #EF4444; --grad-end: #DC2626;">
|
||||
<div class="card-icon">📝</div>
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">Testy z matematyki</h2>
|
||||
<p class="card-desc">Sprawdź swoją wiedzę</p>
|
||||
</div>
|
||||
</a>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer">Version: <span id="commit-sha">(loading)</span></footer>
|
||||
<script src="js/version.js?v=20260521" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,453 +0,0 @@
|
||||
// Minimal app logic for math training
|
||||
const state = {
|
||||
ops: new Set(),
|
||||
mode: null, // 'timed' or 'training'
|
||||
score: 0,
|
||||
currentProblem: null,
|
||||
answerBuffer: '',
|
||||
timerId: null,
|
||||
timeLeft: 60,
|
||||
sessionSolved: 0,
|
||||
sessionTarget: 20,
|
||||
settings: {
|
||||
timedSeconds: 60,
|
||||
maxResult: 40,
|
||||
maxOperand: 20,
|
||||
sessionProblems: 20,
|
||||
allowNegative: false,
|
||||
allowFraction: false,
|
||||
}
|
||||
}
|
||||
|
||||
// elements
|
||||
const menuScreen = document.getElementById('menu-screen')
|
||||
const playScreen = document.getElementById('play-screen')
|
||||
const opsContainer = document.getElementById('ops')
|
||||
const modeButtons = document.querySelectorAll('.mode-btn')
|
||||
const opButtons = document.querySelectorAll('.op-btn')
|
||||
const backBtn = document.getElementById('back-btn')
|
||||
const problemEl = document.getElementById('problem')
|
||||
const answerEl = document.getElementById('answer')
|
||||
const feedbackEl = document.getElementById('feedback')
|
||||
const timerEl = document.getElementById('timer')
|
||||
const scoreEl = document.getElementById('score')
|
||||
const keypad = document.querySelectorAll('.key')
|
||||
const submitBtn = document.getElementById('submit')
|
||||
const clearBtn = document.getElementById('clear')
|
||||
const backspaceBtn = document.getElementById('backspace')
|
||||
const historyPanel = document.getElementById('history-panel')
|
||||
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 progressInner = document.getElementById('progress_inner')
|
||||
const dotBtn = document.getElementById('dot')
|
||||
const negateBtn = document.getElementById('negate')
|
||||
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(){
|
||||
try{
|
||||
let progressInner = null
|
||||
if (raw) {
|
||||
const s = JSON.parse(raw)
|
||||
state.settings = Object.assign(state.settings, s)
|
||||
}
|
||||
}catch(e){ console.warn('settings load failed', e) }
|
||||
// reflect to inputs
|
||||
settingTimed.value = state.settings.timedSeconds
|
||||
settingMaxResult.value = state.settings.maxResult
|
||||
settingMaxOperand.value = state.settings.maxOperand
|
||||
settingSessionProblems.value = state.settings.sessionProblems
|
||||
settingAllowNegative.checked = !!state.settings.allowNegative
|
||||
settingAllowFraction.checked = !!state.settings.allowFraction
|
||||
}
|
||||
|
||||
function saveSettings(){
|
||||
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
|
||||
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, allowNegative: false, allowFraction: false};
|
||||
saveSettings();
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
saveSettingsBtn.addEventListener('click', saveSettings)
|
||||
resetSettingsBtn.addEventListener('click', resetSettings)
|
||||
|
||||
loadSettings()
|
||||
|
||||
// initial wiring
|
||||
opButtons.forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
const op = b.dataset.op
|
||||
if (state.ops.has(op)) { state.ops.delete(op); b.classList.remove('active') }
|
||||
else { state.ops.add(op); b.classList.add('active') }
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
startPlay()
|
||||
}))
|
||||
|
||||
backBtn.addEventListener('click', ()=>{
|
||||
stopTimer()
|
||||
playScreen.classList.add('hidden')
|
||||
menuScreen.classList.remove('hidden')
|
||||
feedbackEl.textContent = ''
|
||||
})
|
||||
|
||||
function startPlay(){
|
||||
menuScreen.classList.add('hidden')
|
||||
playScreen.classList.remove('hidden')
|
||||
state.score = 0
|
||||
scoreEl.textContent = state.score
|
||||
state.answerBuffer = ''
|
||||
answerEl.textContent = ''
|
||||
feedbackEl.textContent = ''
|
||||
state.currentProblem = generateProblem()
|
||||
renderProblem()
|
||||
|
||||
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)
|
||||
updateProgress()
|
||||
timerEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function generateProblem(){
|
||||
const ops = Array.from(state.ops)
|
||||
const op = ops[Math.floor(Math.random()*ops.length)]
|
||||
const maxOp = Math.max(1, state.settings.maxOperand)
|
||||
const maxRes = Math.max(1, state.settings.maxResult)
|
||||
let a = 0, b = 0
|
||||
for (let i=0;i<200;i++){
|
||||
if (op === 'div'){
|
||||
b = randInt(1, maxOp)
|
||||
if (!state.settings.allowFraction){
|
||||
const maxQByOp = Math.floor(maxOp / b)
|
||||
const maxQ = Math.min(maxRes, maxQByOp)
|
||||
if (maxQ < 0) continue
|
||||
const q = randInt(0, maxQ)
|
||||
a = q * b
|
||||
if (state.settings.allowNegative && Math.random() < 0.2) a = -a
|
||||
return {a,b,op}
|
||||
} else {
|
||||
const maxResultNByOp = Math.floor((10 * maxOp) / b)
|
||||
const maxResultN = Math.min(maxRes * 10, maxResultNByOp)
|
||||
if (maxResultN < 0) continue
|
||||
const result_n = randInt(0, maxResultN)
|
||||
if ((result_n * b) % 10 !== 0) continue
|
||||
a = (result_n * b) / 10
|
||||
if (state.settings.allowNegative && Math.random() < 0.2) a = -a
|
||||
return {a,b,op}
|
||||
}
|
||||
} else if (op === 'mul'){
|
||||
a = randInt(0, maxOp)
|
||||
b = randInt(0, maxOp)
|
||||
if (a * b <= maxRes) return {a,b,op}
|
||||
} else if (op === 'add'){
|
||||
a = randInt(0, maxOp)
|
||||
b = randInt(0, maxOp)
|
||||
if (a + b <= maxRes) return {a,b,op}
|
||||
} else if (op === 'sub'){
|
||||
a = randInt(0, maxOp)
|
||||
b = randInt(0, maxOp)
|
||||
if (!state.settings.allowNegative){
|
||||
if (a < b) [a,b] = [b,a]
|
||||
if (a - b <= maxRes) return {a,b,op}
|
||||
} else {
|
||||
if (Math.abs(a - b) <= maxRes) return {a,b,op}
|
||||
}
|
||||
}
|
||||
}
|
||||
a = Math.min(maxOp, Math.floor(maxRes/2))
|
||||
b = Math.min(maxOp, 1)
|
||||
return {a,b,op}
|
||||
}
|
||||
|
||||
function randInt(min,max){
|
||||
return Math.floor(Math.random()*(max-min+1))+min
|
||||
}
|
||||
|
||||
function renderProblem(){
|
||||
const p = state.currentProblem
|
||||
const map = {add: '+', sub: '−', mul: '×', div: '÷'}
|
||||
const symbol = map[p.op] || '?'
|
||||
problemEl.textContent = `${p.a} ${symbol} ${p.b}`
|
||||
}
|
||||
|
||||
function submitAnswer(){
|
||||
const buf = state.answerBuffer.trim()
|
||||
if (buf.length === 0) return
|
||||
const given = parseFloat(buf)
|
||||
const p = state.currentProblem
|
||||
let correct = null
|
||||
if (p.op === 'add') correct = p.a + p.b
|
||||
else if (p.op === 'sub') correct = p.a - p.b
|
||||
else if (p.op === 'mul') correct = p.a * p.b
|
||||
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.classList.add('correct');
|
||||
feedbackEl.classList.remove('incorrect');
|
||||
} else {
|
||||
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
|
||||
state.answerBuffer = ''
|
||||
answerEl.textContent = ''
|
||||
|
||||
if (state.mode === 'training'){
|
||||
state.sessionSolved++
|
||||
updateProgress()
|
||||
if (state.sessionSolved >= state.sessionTarget){
|
||||
saveSessionToHistory()
|
||||
setTimeout(showSummary, 1200);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
feedbackEl.style.opacity = 0;
|
||||
setTimeout(() => {
|
||||
state.currentProblem = generateProblem()
|
||||
renderProblem()
|
||||
}, 200);
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
submitBtn.addEventListener('click', submitAnswer)
|
||||
|
||||
keypad.forEach(k => {
|
||||
k.addEventListener('click', ()=>{
|
||||
const v = k.textContent.trim()
|
||||
if (!/^[0-9]$/.test(v)) return
|
||||
if (state.answerBuffer.length >= 12) return
|
||||
state.answerBuffer += v
|
||||
answerEl.textContent = state.answerBuffer
|
||||
})
|
||||
})
|
||||
|
||||
if (dotBtn){
|
||||
dotBtn.addEventListener('click', ()=>{
|
||||
if (!state.settings.allowFraction) return
|
||||
if (state.answerBuffer.includes('.')) return
|
||||
if (state.answerBuffer.length === 0) state.answerBuffer = '0'
|
||||
state.answerBuffer += '.'
|
||||
answerEl.textContent = state.answerBuffer
|
||||
})
|
||||
}
|
||||
|
||||
if (negateBtn){
|
||||
negateBtn.addEventListener('click', ()=>{
|
||||
if (!state.settings.allowNegative) return
|
||||
if (state.answerBuffer.startsWith('-')) state.answerBuffer = state.answerBuffer.slice(1)
|
||||
else state.answerBuffer = '-' + state.answerBuffer
|
||||
answerEl.textContent = state.answerBuffer
|
||||
})
|
||||
}
|
||||
|
||||
clearBtn.addEventListener('click', ()=>{
|
||||
state.answerBuffer = ''
|
||||
answerEl.textContent = ''
|
||||
})
|
||||
|
||||
backspaceBtn.addEventListener('click', ()=>{
|
||||
state.answerBuffer = state.answerBuffer.slice(0,-1)
|
||||
answerEl.textContent = state.answerBuffer
|
||||
})
|
||||
|
||||
function startTimer(seconds){
|
||||
state.timeLeft = seconds
|
||||
timerEl.textContent = state.timeLeft
|
||||
|
||||
stopTimer();
|
||||
state.timerId = setInterval(()=>{
|
||||
state.timeLeft -= 1
|
||||
timerEl.textContent = state.timeLeft
|
||||
const pct = Math.max(0, (state.timeLeft / seconds) * 100)
|
||||
progressInner.style.width = pct + '%'
|
||||
if (state.timeLeft <= 0) {
|
||||
stopTimer()
|
||||
endSession()
|
||||
}
|
||||
},1000)
|
||||
}
|
||||
|
||||
function stopTimer(){
|
||||
if (state.timerId) clearInterval(state.timerId)
|
||||
state.timerId = null
|
||||
}
|
||||
|
||||
function endSession(){
|
||||
saveSessionToHistory()
|
||||
showSummary();
|
||||
}
|
||||
|
||||
function showSummary(){
|
||||
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');
|
||||
}
|
||||
|
||||
summaryBack.addEventListener('click', ()=>{
|
||||
summaryOverlay.classList.add('hidden');
|
||||
playScreen.classList.add('hidden');
|
||||
menuScreen.classList.remove('hidden');
|
||||
});
|
||||
|
||||
function updateProgress(){
|
||||
if (state.mode === 'training'){
|
||||
const pct = Math.min(100, Math.round((state.sessionSolved/state.sessionTarget)*100))
|
||||
progressInner.style.width = pct + '%'
|
||||
statusEl.textContent = `${state.sessionSolved}/${state.sessionTarget}`
|
||||
}
|
||||
}
|
||||
|
||||
// history persistence
|
||||
function loadHistory(){
|
||||
try{
|
||||
const raw = localStorage.getItem('matma:history')
|
||||
return raw ? JSON.parse(raw) : []
|
||||
}catch(e){ return [] }
|
||||
}
|
||||
|
||||
function saveSessionToHistory(){
|
||||
const h = loadHistory()
|
||||
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()
|
||||
historyPanel.innerHTML = ''
|
||||
if (h.length === 0) {
|
||||
historyPanel.innerHTML = '<p class="history-empty">Brak zapisanych sesji.</p>'
|
||||
return
|
||||
}
|
||||
h.forEach(item => {
|
||||
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 = `
|
||||
<div class="history-item-main">
|
||||
<span class="history-score">Wynik: ${item.score}</span>
|
||||
<span class="history-ops">${opsText}</span>
|
||||
</div>
|
||||
<div class="history-item-meta">
|
||||
<span>${modeText}</span>
|
||||
<span>${d}</span>
|
||||
</div>`
|
||||
historyPanel.appendChild(el)
|
||||
})
|
||||
}
|
||||
|
||||
// Toggle history panel
|
||||
const historyDetails = historyPanel.closest('details');
|
||||
if (historyDetails) {
|
||||
historyDetails.addEventListener('toggle', () => {
|
||||
if (historyDetails.open) {
|
||||
renderHistory();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// keyboard support
|
||||
window.addEventListener('keydown', (e)=>{
|
||||
if (playScreen.classList.contains('hidden')) return
|
||||
if (/^[0-9]$/.test(e.key)){
|
||||
if (state.answerBuffer.length < 12) {
|
||||
state.answerBuffer += e.key
|
||||
answerEl.textContent = state.answerBuffer
|
||||
}
|
||||
} else if (e.key === 'Backspace'){
|
||||
state.answerBuffer = state.answerBuffer.slice(0,-1)
|
||||
answerEl.textContent = state.answerBuffer
|
||||
} else if (e.key === 'Enter'){
|
||||
submitAnswer()
|
||||
} else if (e.key === '.' || e.key === ',') {
|
||||
dotBtn.click();
|
||||
}
|
||||
})
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
// Komponenty HTML loader
|
||||
function loadComponent(path, replaceMap = {}) {
|
||||
return fetch(path)
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
Object.entries(replaceMap).forEach(([key, val]) => {
|
||||
html = html.replaceAll(key, val);
|
||||
});
|
||||
return html;
|
||||
});
|
||||
}
|
||||
|
||||
// Przykład użycia:
|
||||
// loadComponent('components/header.html', {'{TITLE}': 'Tytuł', '{SUBTITLE}': 'Podtytuł'}).then(html => ...)
|
||||
@@ -1,138 +0,0 @@
|
||||
// Nauka Czytania
|
||||
;(function () {
|
||||
// ms between line advances for each speed level (0 = manual)
|
||||
const SPEEDS_MS = [0, 3500, 2000, 1000]
|
||||
const SPEED_LABELS = ['Pauza', 'Wolno', 'Średnio', 'Szybko']
|
||||
|
||||
let yOffset = 0
|
||||
let lineH = 80 // recalculated after render
|
||||
let maxOffset = 0
|
||||
let speedIdx = 0
|
||||
let autoTimer = null
|
||||
|
||||
const listWrap = document.getElementById('list-wrap')
|
||||
const readWrap = document.getElementById('read-wrap')
|
||||
const textList = document.getElementById('text-list')
|
||||
const customInput = document.getElementById('custom-input')
|
||||
const customStartBtn = document.getElementById('custom-start-btn')
|
||||
const readBackBtn = document.getElementById('read-back-btn')
|
||||
const readTitleEl = document.getElementById('read-title')
|
||||
const speedBtn = document.getElementById('speed-btn')
|
||||
const readViewport = document.getElementById('read-viewport')
|
||||
const readTextEl = document.getElementById('read-text')
|
||||
const nextLineBtn = document.getElementById('next-line-btn')
|
||||
let progressBar = null
|
||||
|
||||
// ── Load text list from dyktanda.json ────────────────────────────────────
|
||||
fetch('json/dyktanda.json')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
data.forEach(item => {
|
||||
const btn = document.createElement('button')
|
||||
btn.className = 'list-item-btn'
|
||||
btn.textContent = item.name
|
||||
btn.addEventListener('click', () => startReading(item.name, item.text))
|
||||
textList.appendChild(btn)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
textList.innerHTML = '<p style="color:var(--muted);padding:8px 0">Nie udało się wczytać tekstów.</p>'
|
||||
})
|
||||
|
||||
customStartBtn.addEventListener('click', () => {
|
||||
const txt = customInput.value.trim()
|
||||
if (!txt) return
|
||||
startReading('Własny', txt)
|
||||
})
|
||||
|
||||
// ── Start reading ─────────────────────────────────────────────────────────
|
||||
function startReading(title, text) {
|
||||
yOffset = 0
|
||||
speedIdx = 0
|
||||
clearInterval(autoTimer)
|
||||
autoTimer = null
|
||||
|
||||
readTitleEl.textContent = title
|
||||
readTextEl.textContent = text
|
||||
readTextEl.style.transform = 'translateY(0)'
|
||||
speedBtn.textContent = SPEED_LABELS[0]
|
||||
progressBar.style.width = '0%'
|
||||
|
||||
listWrap.classList.add('hidden')
|
||||
readWrap.classList.remove('hidden')
|
||||
|
||||
// measure real line height after layout
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const cs = getComputedStyle(readTextEl)
|
||||
const lhVal = cs.lineHeight
|
||||
lineH = (lhVal === 'normal')
|
||||
? parseFloat(cs.fontSize) * 1.35
|
||||
: parseFloat(lhVal)
|
||||
|
||||
maxOffset = Math.max(0, readTextEl.offsetHeight - lineH)
|
||||
updateNextBtn()
|
||||
updateProgressBar()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ── Back to list ──────────────────────────────────────────────────────────
|
||||
readBackBtn.addEventListener('click', () => {
|
||||
const active = yOffset > 0 || autoTimer !== null
|
||||
if (active && !confirm('Wrócić do listy tekstów?')) return
|
||||
clearInterval(autoTimer)
|
||||
autoTimer = null
|
||||
readWrap.classList.add('hidden')
|
||||
listWrap.classList.remove('hidden')
|
||||
// Reset speed to manual on exit
|
||||
speedIdx = 0;
|
||||
})
|
||||
|
||||
// ── Speed selector ────────────────────────────────────────────────────────
|
||||
speedBtn.addEventListener('click', () => {
|
||||
clearInterval(autoTimer)
|
||||
autoTimer = null
|
||||
speedIdx = (speedIdx + 1) % SPEEDS_MS.length
|
||||
speedBtn.textContent = SPEED_LABELS[speedIdx]
|
||||
if (SPEEDS_MS[speedIdx] > 0 && yOffset < maxOffset) {
|
||||
autoTimer = setInterval(advanceLine, SPEEDS_MS[speedIdx])
|
||||
}
|
||||
})
|
||||
|
||||
// ── Manual line advance ───────────────────────────────────────────────────
|
||||
nextLineBtn.addEventListener('click', advanceLine)
|
||||
|
||||
// ── Core scroll logic ─────────────────────────────────────────────────────
|
||||
function advanceLine() {
|
||||
if (yOffset >= maxOffset) {
|
||||
stopAutoAtEnd()
|
||||
return
|
||||
}
|
||||
yOffset = Math.min(yOffset + lineH, maxOffset)
|
||||
readTextEl.style.transform = `translateY(${-yOffset}px)`
|
||||
updateNextBtn()
|
||||
updateProgressBar()
|
||||
if (yOffset >= maxOffset) stopAutoAtEnd()
|
||||
}
|
||||
|
||||
function stopAutoAtEnd() {
|
||||
if (autoTimer !== null) {
|
||||
clearInterval(autoTimer)
|
||||
autoTimer = null
|
||||
speedIdx = 0
|
||||
speedBtn.textContent = SPEED_LABELS[0]
|
||||
}
|
||||
updateNextBtn()
|
||||
}
|
||||
|
||||
function updateNextBtn() {
|
||||
nextLineBtn.disabled = yOffset >= maxOffset
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
const progress = maxOffset > 0 ? (yOffset / maxOffset) * 100 : 100
|
||||
if (!progressBar) progressBar = document.getElementById('read-progress-bar-inner')
|
||||
if (progressBar) progressBar.style.width = `${progress}%`
|
||||
}
|
||||
})()
|
||||
@@ -1,320 +0,0 @@
|
||||
// Dyktando — ortografia u/ó, rz/ż, h/ch
|
||||
;(function () {
|
||||
|
||||
// Pairs: key = lowercase match → [choice1, choice2] (shown to user)
|
||||
const PAIRS = {
|
||||
ch: ['ch', 'h'],
|
||||
rz: ['rz', 'ż'],
|
||||
ż: ['rz', 'ż'],
|
||||
ó: ['u', 'ó'],
|
||||
u: ['u', 'ó'],
|
||||
h: ['h', 'ch'],
|
||||
}
|
||||
|
||||
// Order matters: digraphs (ch, rz) must be tried before single chars (h)
|
||||
const BLANK_RE = /ch|rz|[uóżh]/gi
|
||||
|
||||
let tokens = [] // {type:'text',content} | {type:'blank',...}
|
||||
let blanks = [] // subset where type==='blank'
|
||||
let current = 0 // index of active blank
|
||||
let origText = ''
|
||||
|
||||
// ── DOM refs ─────────────────────────────────────────────────────────────
|
||||
const listWrap = document.getElementById('list-wrap')
|
||||
const playWrap = document.getElementById('play-wrap')
|
||||
const textList = document.getElementById('text-list')
|
||||
const customInput = document.getElementById('custom-input')
|
||||
const customStartBtn = document.getElementById('custom-start-btn')
|
||||
const playBackBtn = document.getElementById('play-back-btn')
|
||||
const playTitle = document.getElementById('play-title')
|
||||
const textDisplay = document.getElementById('text-display')
|
||||
const choicesEl = document.getElementById('choices')
|
||||
const progressEl = document.getElementById('blank-progress')
|
||||
const summaryWrap = document.getElementById('summary')
|
||||
const summaryText = document.getElementById('summary-text')
|
||||
const summaryScore = document.getElementById('summary-score')
|
||||
const summaryBackBtn = document.getElementById('summary-back-btn')
|
||||
const dykScroll = document.getElementById('dyk-scroll')
|
||||
let progressBar = null
|
||||
|
||||
// ── Load texts ────────────────────────────────────────────────────────────
|
||||
fetch('json/dyktanda.json')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
data.forEach(item => {
|
||||
const btn = document.createElement('button')
|
||||
btn.className = 'list-item-btn'
|
||||
btn.textContent = item.name
|
||||
btn.addEventListener('click', () => startGame(item.name, item.text))
|
||||
textList.appendChild(btn)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
textList.innerHTML = '<p style="color:var(--muted)">Nie udało się wczytać tekstów.</p>'
|
||||
})
|
||||
|
||||
// Poczekaj na dynamiczne załadowanie progress bara
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// progress bar jest ładowany przez loadComponent w dyktando.html
|
||||
const checkProgressBar = () => {
|
||||
const el = document.getElementById('dyk-progress-bar-inner')
|
||||
if (el) {
|
||||
progressBar = el
|
||||
} else {
|
||||
setTimeout(checkProgressBar, 50)
|
||||
}
|
||||
}
|
||||
checkProgressBar()
|
||||
})
|
||||
|
||||
customStartBtn.addEventListener('click', () => {
|
||||
const txt = customInput.value.trim()
|
||||
if (!txt) return
|
||||
startGame('Własny', txt)
|
||||
})
|
||||
|
||||
// ── Start game ────────────────────────────────────────────────────────────
|
||||
function startGame(title, text) {
|
||||
origText = text
|
||||
tokens = []
|
||||
blanks = []
|
||||
current = 0
|
||||
|
||||
// Tokenize: split text into plain segments and blanks
|
||||
BLANK_RE.lastIndex = 0
|
||||
let lastIdx = 0
|
||||
let m
|
||||
while ((m = BLANK_RE.exec(text)) !== null) {
|
||||
if (m.index > lastIdx) {
|
||||
tokens.push({ type: 'text', content: text.slice(lastIdx, m.index) })
|
||||
}
|
||||
const blank = {
|
||||
type: 'blank',
|
||||
id: blanks.length,
|
||||
answer: m[0], // original (preserves case)
|
||||
choices: PAIRS[m[0].toLowerCase()], // pair to show
|
||||
start: m.index, // position in origText
|
||||
end: m.index + m[0].length,
|
||||
userAnswer: null,
|
||||
correct: null,
|
||||
}
|
||||
tokens.push(blank)
|
||||
blanks.push(blank)
|
||||
lastIdx = m.index + m[0].length
|
||||
}
|
||||
if (lastIdx < text.length) {
|
||||
tokens.push({ type: 'text', content: text.slice(lastIdx) })
|
||||
}
|
||||
|
||||
// Reset UI
|
||||
playTitle.textContent = title
|
||||
summaryWrap.classList.add('hidden')
|
||||
textDisplay.classList.remove('hidden')
|
||||
choicesEl.classList.remove('hidden')
|
||||
progressEl.textContent = ''
|
||||
if (progressBar) progressBar.style.width = '0%'
|
||||
|
||||
listWrap.classList.add('hidden')
|
||||
playWrap.classList.remove('hidden')
|
||||
|
||||
if (blanks.length === 0) {
|
||||
renderText()
|
||||
showSummary()
|
||||
} else {
|
||||
renderText()
|
||||
activateBlank(0)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render the text with blanks ───────────────────────────────────────────
|
||||
function renderText() {
|
||||
textDisplay.innerHTML = ''
|
||||
tokens.forEach(tok => {
|
||||
if (tok.type === 'text') {
|
||||
textDisplay.appendChild(document.createTextNode(tok.content))
|
||||
} else {
|
||||
const span = document.createElement('span')
|
||||
span.id = `blank-${tok.id}`
|
||||
|
||||
if (tok.correct !== null) {
|
||||
// Already answered
|
||||
if (tok.correct) {
|
||||
span.className = 'dyk-blank dyk-blank--ok'
|
||||
span.textContent = tok.answer
|
||||
} else {
|
||||
span.className = 'dyk-blank dyk-blank--err'
|
||||
span.innerHTML = `<s>${esc(tok.userAnswer)}</s><sup class="dyk-correction">${esc(tok.answer)}</sup>`
|
||||
}
|
||||
} else if (tok.id === current) {
|
||||
span.className = 'dyk-blank dyk-blank--active'
|
||||
span.textContent = '__'
|
||||
} else {
|
||||
span.className = 'dyk-blank dyk-blank--pending'
|
||||
span.textContent = '__'
|
||||
}
|
||||
|
||||
textDisplay.appendChild(span)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Activate a blank ──────────────────────────────────────────────────────
|
||||
function activateBlank(idx) {
|
||||
current = idx
|
||||
renderText()
|
||||
|
||||
const blank = blanks[idx]
|
||||
|
||||
// Shuffle choices so correct answer isn't always in same position
|
||||
const choices = Math.random() < 0.5
|
||||
? [...blank.choices]
|
||||
: [...blank.choices].reverse()
|
||||
|
||||
choicesEl.innerHTML = ''
|
||||
choices.forEach(choice => {
|
||||
const btn = document.createElement('button')
|
||||
btn.className = 'choice-btn'
|
||||
btn.textContent = choice
|
||||
btn.addEventListener('click', () => handleAnswer(blank, choice))
|
||||
choicesEl.appendChild(btn)
|
||||
})
|
||||
|
||||
progressEl.textContent = `${idx + 1} / ${blanks.length}`
|
||||
|
||||
// Scroll active blank into view
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById(`blank-${idx}`)
|
||||
if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
})
|
||||
|
||||
updateProgressBar()
|
||||
}
|
||||
|
||||
// ── Handle answer ─────────────────────────────────────────────────────────
|
||||
function handleAnswer(blank, choice) {
|
||||
blank.correct = choice.toLowerCase() === blank.answer.toLowerCase()
|
||||
blank.userAnswer = choice
|
||||
|
||||
const nextIdx = blank.id + 1
|
||||
if (nextIdx >= blanks.length) {
|
||||
// Last blank answered: show final state briefly, then summary
|
||||
current = blanks.length
|
||||
choicesEl.innerHTML = ''
|
||||
progressEl.textContent = ''
|
||||
renderText()
|
||||
setTimeout(showSummary, 500)
|
||||
} else {
|
||||
activateBlank(nextIdx)
|
||||
}
|
||||
updateProgressBar()
|
||||
}
|
||||
|
||||
// ── Summary ───────────────────────────────────────────────────────────────
|
||||
function showSummary() {
|
||||
choicesEl.classList.add('hidden')
|
||||
textDisplay.classList.add('hidden')
|
||||
summaryWrap.classList.remove('hidden')
|
||||
|
||||
const total = blanks.length
|
||||
const correct = blanks.filter(b => b.correct).length
|
||||
|
||||
if (total === 0) {
|
||||
summaryScore.textContent = 'Brak liter do uzupełnienia w tym tekście.'
|
||||
summaryText.innerHTML = esc(origText)
|
||||
updateProgressBar()
|
||||
} else {
|
||||
summaryScore.textContent = `Poprawnie: ${correct} z ${total}`
|
||||
summaryText.innerHTML = buildSummaryHTML()
|
||||
}
|
||||
|
||||
dykScroll.scrollTop = 0
|
||||
}
|
||||
|
||||
// Build word-coloured summary HTML
|
||||
function buildSummaryHTML() {
|
||||
// Find word spans (non-whitespace sequences) in original text
|
||||
const wordRe = /\S+/g
|
||||
const words = []
|
||||
let wm
|
||||
while ((wm = wordRe.exec(origText)) !== null) {
|
||||
words.push({ start: wm.index, end: wm.index + wm[0].length })
|
||||
}
|
||||
|
||||
// Assign each word its blanks and overall status
|
||||
words.forEach(w => {
|
||||
w.blanks = blanks.filter(b => b.start >= w.start && b.end <= w.end)
|
||||
w.status = w.blanks.length === 0 ? 'none'
|
||||
: w.blanks.every(b => b.correct) ? 'ok' : 'err'
|
||||
})
|
||||
|
||||
let html = ''
|
||||
let pos = 0
|
||||
|
||||
words.forEach(word => {
|
||||
// Whitespace / punctuation before this word
|
||||
if (word.start > pos) html += esc(origText.slice(pos, word.start))
|
||||
|
||||
// Build inner word HTML (blanks replaced by correct answers, coloured)
|
||||
let wPos = word.start
|
||||
let inner = ''
|
||||
word.blanks.forEach(blank => {
|
||||
if (blank.start > wPos) inner += esc(origText.slice(wPos, blank.start))
|
||||
if (blank.correct) {
|
||||
inner += `<span class="dyk-word--ok">${esc(blank.answer)}</span>`
|
||||
} else {
|
||||
inner += `<span class="dyk-word--err"><s>${esc(blank.userAnswer)}</s><sup class="dyk-correction">${esc(blank.answer)}</sup></span>`
|
||||
}
|
||||
wPos = blank.end
|
||||
})
|
||||
inner += esc(origText.slice(wPos, word.end))
|
||||
|
||||
html += word.status !== 'none'
|
||||
? `<span class="dyk-word--${word.status}">${inner}</span>`
|
||||
: inner
|
||||
|
||||
pos = word.end
|
||||
})
|
||||
|
||||
if (pos < origText.length) html += esc(origText.slice(pos))
|
||||
return html
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
const total = blanks.length
|
||||
if (total === 0) {
|
||||
progressBar.style.width = '100%'
|
||||
return
|
||||
}
|
||||
// Use `current` which is the index of the *next* blank to be filled.
|
||||
// When the game ends, `current` becomes `blanks.length`.
|
||||
const progress = (current / total) * 100
|
||||
progressBar.style.width = `${progress}%`
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return (s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────
|
||||
playBackBtn.addEventListener('click', () => {
|
||||
const inProgress = blanks.length > 0 && current < blanks.length
|
||||
if (inProgress && !confirm('Przerwać dyktando i wrócić do listy?')) return
|
||||
goToList()
|
||||
})
|
||||
|
||||
summaryBackBtn.addEventListener('click', goToList)
|
||||
|
||||
function goToList() {
|
||||
tokens = []
|
||||
blanks = []
|
||||
current = 0
|
||||
textDisplay.innerHTML = ''
|
||||
textDisplay.classList.remove('hidden')
|
||||
summaryWrap.classList.add('hidden')
|
||||
choicesEl.innerHTML = ''
|
||||
choicesEl.classList.remove('hidden')
|
||||
playWrap.classList.add('hidden')
|
||||
listWrap.classList.remove('hidden')
|
||||
}
|
||||
|
||||
})()
|
||||
@@ -1,149 +0,0 @@
|
||||
// Nauka Dzielenia
|
||||
;(function () {
|
||||
const st = { divisor: null, total: 20, solved: 0, score: 0, current: null, buf: '' }
|
||||
|
||||
const selectScreen = document.getElementById('select-screen')
|
||||
const playScreen = document.getElementById('play-screen')
|
||||
const summaryScreen = document.getElementById('summary-screen')
|
||||
const problemEl = document.getElementById('problem')
|
||||
const answerEl = document.getElementById('answer')
|
||||
const feedbackEl = document.getElementById('feedback')
|
||||
let progressInner = null
|
||||
const progressLabel = document.getElementById('progress-label')
|
||||
const summaryText = document.getElementById('summary-text')
|
||||
const totalInput = document.getElementById('total-input')
|
||||
|
||||
// divisor selection
|
||||
document.getElementById('table-grid').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.table-btn')
|
||||
if (!btn) return
|
||||
document.querySelectorAll('.table-btn').forEach(b => b.classList.remove('active'))
|
||||
btn.classList.add('active')
|
||||
st.divisor = parseInt(btn.dataset.val, 10) // 0 = all
|
||||
})
|
||||
|
||||
document.getElementById('start-btn').addEventListener('click', () => {
|
||||
st.total = Math.max(5, Math.min(100, parseInt(totalInput.value, 10) || 20))
|
||||
st.solved = 0
|
||||
st.score = 0
|
||||
show(playScreen)
|
||||
nextProblem()
|
||||
})
|
||||
|
||||
document.getElementById('back-btn').addEventListener('click', () => show(selectScreen))
|
||||
document.getElementById('again-btn').addEventListener('click', () => {
|
||||
st.solved = 0; st.score = 0; show(playScreen); nextProblem()
|
||||
})
|
||||
document.getElementById('change-btn').addEventListener('click', () => show(selectScreen))
|
||||
|
||||
// keypad
|
||||
document.querySelectorAll('.key').forEach(k => {
|
||||
k.addEventListener('click', () => {
|
||||
const v = k.textContent.trim()
|
||||
if (!/^[0-9]$/.test(v)) return
|
||||
if (st.buf.length >= 6) return
|
||||
st.buf += v; answerEl.textContent = st.buf
|
||||
})
|
||||
})
|
||||
document.getElementById('clear').addEventListener('click', () => { st.buf = ''; answerEl.textContent = '' })
|
||||
document.getElementById('backspace').addEventListener('click', () => { st.buf = st.buf.slice(0,-1); answerEl.textContent = st.buf || '' })
|
||||
document.getElementById('submit').addEventListener('click', submit)
|
||||
|
||||
window.addEventListener('keydown', e => {
|
||||
if (!playScreen.classList.contains('hidden')) {
|
||||
if (/^[0-9]$/.test(e.key) && st.buf.length < 6) { st.buf += e.key; answerEl.textContent = st.buf }
|
||||
else if (e.key === 'Backspace') { st.buf = st.buf.slice(0,-1); answerEl.textContent = st.buf || '' }
|
||||
else if (e.key === 'Enter') submit()
|
||||
}
|
||||
})
|
||||
|
||||
function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min }
|
||||
|
||||
function nextProblem() {
|
||||
const b = st.divisor || randInt(1, 10) // divisor
|
||||
const answer = randInt(1, 10) // quotient (always integer)
|
||||
const a = b * answer // dividend
|
||||
st.current = { a, b, answer }
|
||||
problemEl.textContent = `${a} ÷ ${b}`
|
||||
feedbackEl.textContent = ''
|
||||
st.buf = ''; answerEl.textContent = ''
|
||||
updateBar()
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!st.buf.trim()) return
|
||||
const given = parseInt(st.buf, 10)
|
||||
st.solved++
|
||||
if (given === st.current.answer) {
|
||||
st.score++
|
||||
feedbackEl.textContent = 'Dobrze!'
|
||||
feedbackEl.classList.add('correct')
|
||||
feedbackEl.classList.remove('incorrect')
|
||||
} else {
|
||||
feedbackEl.textContent = `Poprawna odpowiedź: ${st.current.answer}`
|
||||
feedbackEl.classList.add('incorrect')
|
||||
feedbackEl.classList.remove('correct')
|
||||
}
|
||||
feedbackEl.style.opacity = 1;
|
||||
st.buf = ''; answerEl.textContent = ''
|
||||
updateBar()
|
||||
if (st.solved >= st.total) {
|
||||
setTimeout(showSummary, 1200)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
feedbackEl.style.opacity = 0;
|
||||
setTimeout(nextProblem, 200);
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
function updateBar() {
|
||||
const pct = st.total > 0 ? Math.round((st.solved / st.total) * 100) : 0
|
||||
if (!progressInner) {
|
||||
progressInner = document.getElementById('progress-inner')
|
||||
}
|
||||
if (progressInner) progressInner.style.width = pct + '%'
|
||||
progressLabel.textContent = `${st.solved}/${st.total}`
|
||||
}
|
||||
|
||||
function showSummary() {
|
||||
const pct = st.total > 0 ? Math.round((st.score / st.total) * 100) : 0
|
||||
summaryText.textContent = `Twój wynik: ${st.score} / ${st.total} poprawnie (${pct}%)`
|
||||
show(summaryScreen)
|
||||
}
|
||||
|
||||
function show(screen) {
|
||||
[selectScreen, playScreen, summaryScreen].forEach(s => s.classList.add('hidden'))
|
||||
screen.classList.remove('hidden')
|
||||
if (screen === summaryScreen) {
|
||||
selectScreen.classList.add('hidden');
|
||||
playScreen.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// commit SHA and default selection
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const el = document.getElementById('commit-sha')
|
||||
if (!el) return
|
||||
|
||||
// Set default divisor selection
|
||||
const defaultDivisorBtn = document.querySelector('.table-btn[data-val="0"]');
|
||||
if(defaultDivisorBtn) {
|
||||
defaultDivisorBtn.classList.add('active');
|
||||
st.divisor = 0;
|
||||
}
|
||||
|
||||
let sha = (window.COMMIT_SHA || '').toString().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) {}
|
||||
}
|
||||
if (sha) el.textContent = sha.slice(0, 8)
|
||||
})
|
||||
})()
|
||||
@@ -1,116 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>Nauka Mnożenia</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=20260524" />
|
||||
<link rel="stylesheet" href="css/fonts/inter.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- ── SELECT SCREEN ── -->
|
||||
<main id="select-screen">
|
||||
<nav class="subpage-nav">
|
||||
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
|
||||
</nav>
|
||||
<div class="screen-content">
|
||||
<div id="header-placeholder"></div>
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">Wybór tabliczki</h2>
|
||||
<div class="table-grid" id="table-grid">
|
||||
<button class="table-btn" data-val="1">1</button>
|
||||
<button class="table-btn" data-val="2">2</button>
|
||||
<button class="table-btn" data-val="3">3</button>
|
||||
<button class="table-btn" data-val="4">4</button>
|
||||
<button class="table-btn" data-val="5">5</button>
|
||||
<button class="table-btn" data-val="6">6</button>
|
||||
<button class="table-btn" data-val="7">7</button>
|
||||
<button class="table-btn" data-val="8">8</button>
|
||||
<button class="table-btn" data-val="9">9</button>
|
||||
<button class="table-btn" data-val="10">10</button>
|
||||
<button class="table-btn all" data-val="0">Wszystkie</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="content-panel">
|
||||
<label class="settings-label">
|
||||
Liczba zadań w serii:
|
||||
<input id="total-input" type="number" min="5" max="100" value="20" class="settings-input">
|
||||
</label>
|
||||
</section>
|
||||
<button class="action-btn start-btn" id="start-btn">Rozpocznij ćwiczenie →</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── PLAY SCREEN ── -->
|
||||
<main class="play-screen-container hidden" id="play-screen">
|
||||
<header class="read-header">
|
||||
<button id="back-btn" class="read-nav-btn">← Zmień</button>
|
||||
<span class="read-title-text">Mnożenie</span>
|
||||
<div class="progress-container">
|
||||
<span id="progress-label" class="progress-text">0/20</span>
|
||||
</div>
|
||||
</header>
|
||||
<div id="progressbar-placeholder"></div>
|
||||
|
||||
<div class="play-body">
|
||||
<div class="problem-display">
|
||||
<div id="problem" class="problem-text">?</div>
|
||||
<div id="feedback" class="feedback-text"></div>
|
||||
</div>
|
||||
|
||||
<div class="keypad-container">
|
||||
<div id="answer" class="answer-display"></div>
|
||||
<div class="keypad">
|
||||
<div class="key-row">
|
||||
<button class="key">1</button><button class="key">2</button><button class="key">3</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">4</button><button class="key">5</button><button class="key">6</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">7</button><button class="key">8</button><button class="key">9</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key key-special" id="clear">C</button>
|
||||
<button class="key">0</button>
|
||||
<button class="key key-special" id="backspace">⌫</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="submit-btn" id="submit">Sprawdź</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── SUMMARY SCREEN ── -->
|
||||
<main class="screen-content hidden" id="summary-screen" style="margin-top: 24px;">
|
||||
<div class="summary-view">
|
||||
<div style="font-size: 64px; text-align: center;">🎉</div>
|
||||
<h2 style="text-align: center; font-size: 28px; margin: 16px 0;">Koniec ćwiczenia!</h2>
|
||||
<p id="summary-text" class="summary-score-text"></p>
|
||||
<button class="action-btn summary-back-btn" id="again-btn">Jeszcze raz</button>
|
||||
<button class="action-btn summary-back-btn secondary" id="change-btn">Zmień tabliczkę</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="footer-placeholder"></div>
|
||||
<script src="js/nav.js?v=20260521"></script>
|
||||
<script src="js/mnozenie.js?v=20260524"></script>
|
||||
<script src="js/components.js"></script>
|
||||
<script>
|
||||
loadComponent('components/header.html', {
|
||||
'{TITLE}': '× Nauka Mnożenia',
|
||||
'{SUBTITLE}': 'Wybierz tabliczkę, którą chcesz poćwiczyć.'
|
||||
}).then(html => {
|
||||
document.getElementById('header-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/progress-bar.html', {'{PROGRESS_ID}': 'progress-inner'}).then(html => {
|
||||
document.getElementById('progressbar-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/footer.html').then(html => {
|
||||
document.getElementById('footer-placeholder').outerHTML = html;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,170 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>Testy Matematyczne</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=20260524" />
|
||||
<link rel="stylesheet" href="css/fonts/inter.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- ── MENU SCREEN ── -->
|
||||
<main id="menu-screen">
|
||||
<nav class="subpage-nav">
|
||||
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
|
||||
</nav>
|
||||
<div class="screen-content">
|
||||
<div id="header-placeholder"></div>
|
||||
|
||||
<div class="test-config-grid">
|
||||
<!-- Operations -->
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">1. Wybierz działania</h2>
|
||||
<div class="ops-grid" id="ops">
|
||||
<button class="op-btn" data-op="add">+ Dodawanie</button>
|
||||
<button class="op-btn" data-op="sub">− Odejmowanie</button>
|
||||
<button class="op-btn" data-op="mul">× Mnożenie</button>
|
||||
<button class="op-btn" data-op="div">÷ Dzielenie</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Game Mode -->
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">2. Wybierz tryb gry</h2>
|
||||
<div class="modes-grid">
|
||||
<button class="mode-btn" data-mode="timed" id="mode-timed">
|
||||
<span class="mode-icon">⧗</span>
|
||||
<span class="mode-label">Na czas</span>
|
||||
</button>
|
||||
<button class="mode-btn" data-mode="training" id="mode-training">
|
||||
<span class="mode-icon">∞</span>
|
||||
<span class="mode-label">Trening</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="hint-text">Wybierz co najmniej jedno działanie, a następnie tryb, aby rozpocząć.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<section class="content-panel">
|
||||
<details class="settings-details">
|
||||
<summary class="settings-summary">
|
||||
<h2 class="panel-title">Ustawienia zaawansowane</h2>
|
||||
</summary>
|
||||
<div class="settings-content" id="settings-panel">
|
||||
<label class="settings-label-row"> Czas (sek) — tryb na czas
|
||||
<input id="setting-timed" type="number" min="5" max="600" class="settings-input-row" />
|
||||
</label>
|
||||
<label class="settings-label-row"> Maksymalny wynik
|
||||
<input id="setting-max-result" type="number" min="1" max="999" class="settings-input-row" />
|
||||
</label>
|
||||
<label class="settings-label-row"> Maksymalna składowa (operand)
|
||||
<input id="setting-max-operand" type="number" min="1" max="999" class="settings-input-row" />
|
||||
</label>
|
||||
<label class="settings-label-row"> Liczba zadań (tryb Trening)
|
||||
<input id="setting-session-problems" type="number" min="1" max="500" class="settings-input-row" />
|
||||
</label>
|
||||
<label class="settings-label-row checkbox"> Wynik może być ujemny
|
||||
<input id="setting-allow-negative" type="checkbox" />
|
||||
</label>
|
||||
<label class="settings-label-row checkbox"> Wynik może być ułamkiem
|
||||
<input id="setting-allow-fraction" type="checkbox" />
|
||||
</label>
|
||||
<div class="settings-actions">
|
||||
<button id="save-settings" class="action-btn">Zapisz</button>
|
||||
<button id="reset-settings" class="action-btn secondary">Resetuj</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<!-- History -->
|
||||
<section class="content-panel">
|
||||
<details class="settings-details">
|
||||
<summary class="settings-summary">
|
||||
<h2 class="panel-title">Historia wyników</h2>
|
||||
</summary>
|
||||
<div class="history-list" id="history-panel">
|
||||
<!-- History items will be injected here -->
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── PLAY SCREEN ── -->
|
||||
<main class="play-screen-container hidden" id="play-screen">
|
||||
<header class="play-header">
|
||||
<button id="back-btn" class="play-nav-btn">← Menu</button>
|
||||
<div id="status" class="play-status">Trening</div>
|
||||
<div id="score" class="play-score">0</div>
|
||||
</header>
|
||||
<div id="progressbar-placeholder"></div>
|
||||
|
||||
<div class="play-body">
|
||||
<div class="problem-display">
|
||||
<div id="timer" class="timer-display hidden">60</div>
|
||||
<div id="problem" class="problem-text">—</div>
|
||||
<div id="feedback" class="feedback-text"></div>
|
||||
</div>
|
||||
|
||||
<div class="keypad-container">
|
||||
<div id="answer" class="answer-display"></div>
|
||||
<div class="keypad">
|
||||
<div class="key-row">
|
||||
<button class="key">1</button><button class="key">2</button><button class="key">3</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">4</button><button class="key">5</button><button class="key">6</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">7</button><button class="key">8</button><button class="key">9</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key key-special" id="negate">±</button>
|
||||
<button class="key">0</button>
|
||||
<button class="key key-special" id="dot">.</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key key-special" id="clear">C</button>
|
||||
<button class="key key-special" id="backspace">⌫</button>
|
||||
<button class="submit-btn" id="submit">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── SUMMARY SCREEN ── -->
|
||||
<div id="summary-overlay" class="summary-overlay hidden">
|
||||
<div class="summary-modal">
|
||||
<div style="font-size: 48px;">🎉</div>
|
||||
<h2>Koniec gry!</h2>
|
||||
<p id="summary-text" class="summary-modal-text"></p>
|
||||
<button id="summary-back" class="action-btn">Wróć do menu</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer-placeholder"></div>
|
||||
<script src="js/version.js?v=20260521" defer></script>
|
||||
<script src="js/nav.js?v=20260521"></script>
|
||||
<script src="js/app.js?v=20260524"></script>
|
||||
<script src="js/components.js"></script>
|
||||
<script>
|
||||
loadComponent('components/header.html', {
|
||||
'{TITLE}': '📝 Testy Matematyczne',
|
||||
'{SUBTITLE}': 'Skonfiguruj swój test i sprawdź wiedzę.'
|
||||
}).then(html => {
|
||||
document.getElementById('header-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/progress-bar.html', {'{PROGRESS_ID}': 'progress_inner'}).then(html => {
|
||||
document.getElementById('progressbar-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/footer.html').then(html => {
|
||||
document.getElementById('footer-placeholder').outerHTML = html;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "COMPATIBLE_SCREEN_MANIFEST",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.example.app",
|
||||
"variantName": "release",
|
||||
"elements": []
|
||||
}
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-117
@@ -1,117 +0,0 @@
|
||||
# This is a configuration file for ProGuard.
|
||||
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
||||
#
|
||||
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
|
||||
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
|
||||
# will be ignored by new version of the Android plugin for Gradle.
|
||||
|
||||
# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file
|
||||
# instead of this one, which turns off the optimization flags.
|
||||
# Adding optimization introduces certain risks, since for example not all optimizations performed by
|
||||
# ProGuard works on all versions of Dalvik. The following flags turn off various optimizations
|
||||
# known to have issues, but the list may not be complete or up to date. (The "arithmetic"
|
||||
# optimization can be used if you are only targeting Android 2.0 or later.) Make sure you test
|
||||
# thoroughly if you go this route.
|
||||
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||
-optimizationpasses 5
|
||||
-allowaccessmodification
|
||||
|
||||
-dontusemixedcaseclassnames
|
||||
-dontskipnonpubliclibraryclasses
|
||||
-verbose
|
||||
|
||||
# Preserve some attributes that may be required for reflection.
|
||||
-keepattributes AnnotationDefault,
|
||||
EnclosingMethod,
|
||||
InnerClasses,
|
||||
RuntimeVisibleAnnotations,
|
||||
RuntimeVisibleParameterAnnotations,
|
||||
RuntimeVisibleTypeAnnotations,
|
||||
Signature
|
||||
|
||||
-keep public class com.google.vending.licensing.ILicensingService
|
||||
-keep public class com.android.vending.licensing.ILicensingService
|
||||
-keep public class com.google.android.vending.licensing.ILicensingService
|
||||
-dontnote com.android.vending.licensing.ILicensingService
|
||||
-dontnote com.google.vending.licensing.ILicensingService
|
||||
-dontnote com.google.android.vending.licensing.ILicensingService
|
||||
|
||||
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# Keep setters in Views so that animations can still work.
|
||||
-keepclassmembers public class * extends android.view.View {
|
||||
void set*(***);
|
||||
*** get*();
|
||||
}
|
||||
|
||||
# We want to keep methods in Activity that could be used in the XML attribute onClick.
|
||||
-keepclassmembers class * extends android.app.Activity {
|
||||
public void *(android.view.View);
|
||||
}
|
||||
|
||||
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
# Preserve annotated Javascript interface methods.
|
||||
-keepclassmembers class * {
|
||||
@android.webkit.JavascriptInterface <methods>;
|
||||
}
|
||||
|
||||
# The support libraries contains references to newer platform versions.
|
||||
# Don't warn about those in case this app is linking against an older
|
||||
# platform version. We know about them, and they are safe.
|
||||
-dontnote android.support.**
|
||||
-dontnote androidx.**
|
||||
-dontwarn android.support.**
|
||||
-dontwarn androidx.**
|
||||
|
||||
# This class is deprecated, but remains for backward compatibility.
|
||||
-dontwarn android.util.FloatMath
|
||||
|
||||
# Understand the @Keep support annotation.
|
||||
-keep class android.support.annotation.Keep
|
||||
-keep class androidx.annotation.Keep
|
||||
|
||||
-keep @android.support.annotation.Keep class * {*;}
|
||||
-keep @androidx.annotation.Keep class * {*;}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@android.support.annotation.Keep <methods>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <methods>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@android.support.annotation.Keep <fields>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <fields>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@android.support.annotation.Keep <init>(...);
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <init>(...);
|
||||
}
|
||||
|
||||
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
|
||||
-dontnote org.apache.http.**
|
||||
-dontnote android.net.http.**
|
||||
|
||||
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
|
||||
-dontnote java.lang.invoke.**
|
||||
@@ -1,116 +0,0 @@
|
||||
# This is a configuration file for ProGuard.
|
||||
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
||||
#
|
||||
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
|
||||
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
|
||||
# will be ignored by new version of the Android plugin for Gradle.
|
||||
|
||||
# Optimization is turned off by default. Dex does not like code run
|
||||
# through the ProGuard optimize steps (and performs some
|
||||
# of these optimizations on its own).
|
||||
# Note that if you want to enable optimization, you cannot just
|
||||
# include optimization flags in your own project configuration file;
|
||||
# instead you will need to point to the
|
||||
# "proguard-android-optimize.txt" file instead of this one from your
|
||||
# project.properties file.
|
||||
-dontoptimize
|
||||
|
||||
-dontusemixedcaseclassnames
|
||||
-dontskipnonpubliclibraryclasses
|
||||
-verbose
|
||||
|
||||
# Preserve some attributes that may be required for reflection.
|
||||
-keepattributes AnnotationDefault,
|
||||
EnclosingMethod,
|
||||
InnerClasses,
|
||||
RuntimeVisibleAnnotations,
|
||||
RuntimeVisibleParameterAnnotations,
|
||||
RuntimeVisibleTypeAnnotations,
|
||||
Signature
|
||||
|
||||
-keep public class com.google.vending.licensing.ILicensingService
|
||||
-keep public class com.android.vending.licensing.ILicensingService
|
||||
-keep public class com.google.android.vending.licensing.ILicensingService
|
||||
-dontnote com.android.vending.licensing.ILicensingService
|
||||
-dontnote com.google.vending.licensing.ILicensingService
|
||||
-dontnote com.google.android.vending.licensing.ILicensingService
|
||||
|
||||
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# Keep setters in Views so that animations can still work.
|
||||
-keepclassmembers public class * extends android.view.View {
|
||||
void set*(***);
|
||||
*** get*();
|
||||
}
|
||||
|
||||
# We want to keep methods in Activity that could be used in the XML attribute onClick.
|
||||
-keepclassmembers class * extends android.app.Activity {
|
||||
public void *(android.view.View);
|
||||
}
|
||||
|
||||
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
# Preserve annotated Javascript interface methods.
|
||||
-keepclassmembers class * {
|
||||
@android.webkit.JavascriptInterface <methods>;
|
||||
}
|
||||
|
||||
# The support libraries contains references to newer platform versions.
|
||||
# Don't warn about those in case this app is linking against an older
|
||||
# platform version. We know about them, and they are safe.
|
||||
-dontnote android.support.**
|
||||
-dontnote androidx.**
|
||||
-dontwarn android.support.**
|
||||
-dontwarn androidx.**
|
||||
|
||||
# This class is deprecated, but remains for backward compatibility.
|
||||
-dontwarn android.util.FloatMath
|
||||
|
||||
# Understand the @Keep support annotation.
|
||||
-keep class android.support.annotation.Keep
|
||||
-keep class androidx.annotation.Keep
|
||||
|
||||
-keep @android.support.annotation.Keep class * {*;}
|
||||
-keep @androidx.annotation.Keep class * {*;}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@android.support.annotation.Keep <methods>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <methods>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@android.support.annotation.Keep <fields>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <fields>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@android.support.annotation.Keep <init>(...);
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <init>(...);
|
||||
}
|
||||
|
||||
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
|
||||
-dontnote org.apache.http.**
|
||||
-dontnote android.net.http.**
|
||||
|
||||
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
|
||||
-dontnote java.lang.invoke.**
|
||||
@@ -1,117 +0,0 @@
|
||||
# This is a configuration file for ProGuard.
|
||||
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
||||
#
|
||||
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
|
||||
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
|
||||
# will be ignored by new version of the Android plugin for Gradle.
|
||||
|
||||
# Optimizations can be turned on and off in the 'postProcessing' DSL block.
|
||||
# The configuration below is applied if optimizations are enabled.
|
||||
# Adding optimization introduces certain risks, since for example not all optimizations performed by
|
||||
# ProGuard works on all versions of Dalvik. The following flags turn off various optimizations
|
||||
# known to have issues, but the list may not be complete or up to date. (The "arithmetic"
|
||||
# optimization can be used if you are only targeting Android 2.0 or later.) Make sure you test
|
||||
# thoroughly if you go this route.
|
||||
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||
-optimizationpasses 5
|
||||
-allowaccessmodification
|
||||
|
||||
-dontusemixedcaseclassnames
|
||||
-dontskipnonpubliclibraryclasses
|
||||
-verbose
|
||||
|
||||
# Preserve some attributes that may be required for reflection.
|
||||
-keepattributes AnnotationDefault,
|
||||
EnclosingMethod,
|
||||
InnerClasses,
|
||||
RuntimeVisibleAnnotations,
|
||||
RuntimeVisibleParameterAnnotations,
|
||||
RuntimeVisibleTypeAnnotations,
|
||||
Signature
|
||||
|
||||
-keep public class com.google.vending.licensing.ILicensingService
|
||||
-keep public class com.android.vending.licensing.ILicensingService
|
||||
-keep public class com.google.android.vending.licensing.ILicensingService
|
||||
-dontnote com.android.vending.licensing.ILicensingService
|
||||
-dontnote com.google.vending.licensing.ILicensingService
|
||||
-dontnote com.google.android.vending.licensing.ILicensingService
|
||||
|
||||
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# Keep setters in Views so that animations can still work.
|
||||
-keepclassmembers public class * extends android.view.View {
|
||||
void set*(***);
|
||||
*** get*();
|
||||
}
|
||||
|
||||
# We want to keep methods in Activity that could be used in the XML attribute onClick.
|
||||
-keepclassmembers class * extends android.app.Activity {
|
||||
public void *(android.view.View);
|
||||
}
|
||||
|
||||
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
# Preserve annotated Javascript interface methods.
|
||||
-keepclassmembers class * {
|
||||
@android.webkit.JavascriptInterface <methods>;
|
||||
}
|
||||
|
||||
# The support libraries contains references to newer platform versions.
|
||||
# Don't warn about those in case this app is linking against an older
|
||||
# platform version. We know about them, and they are safe.
|
||||
-dontnote android.support.**
|
||||
-dontnote androidx.**
|
||||
-dontwarn android.support.**
|
||||
-dontwarn androidx.**
|
||||
|
||||
# This class is deprecated, but remains for backward compatibility.
|
||||
-dontwarn android.util.FloatMath
|
||||
|
||||
# Understand the @Keep support annotation.
|
||||
-keep class android.support.annotation.Keep
|
||||
-keep class androidx.annotation.Keep
|
||||
|
||||
-keep @android.support.annotation.Keep class * {*;}
|
||||
-keep @androidx.annotation.Keep class * {*;}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@android.support.annotation.Keep <methods>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <methods>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@android.support.annotation.Keep <fields>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <fields>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@android.support.annotation.Keep <init>(...);
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <init>(...);
|
||||
}
|
||||
|
||||
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
|
||||
-dontnote org.apache.http.**
|
||||
-dontnote android.net.http.**
|
||||
|
||||
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
|
||||
-dontnote java.lang.invoke.**
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user