diff --git a/README.md b/README.md index afd3a86..4fbef44 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ fizika/ 3. **Access the applications:** - **Student Quiz**: http://localhost (or your domain) - - **Admin Panel**: http://localhost:3001/admin.html + - **Admin Panel**: http://localhost:3001/ ### Option 2: Local Development @@ -87,15 +87,20 @@ NODE_ENV=production # Frontend integration FRONTEND_URL=* + +# File paths (relative to backend directory) +DATA_PATH=../fizika.json +PICS_PATH=../pics ``` ## 🔌 API Integration -The frontend automatically detects the backend: -- **Local Development**: `http://localhost:3001` -- **Production**: Configure `API_BASE` in `js/load.js` +The frontend **prioritizes backend API** with local fallback: +- **Primary**: Auto-detects backend (`localhost:3001` for dev, same origin for production) +- **Fallback**: Local `fizika.json` and `pics/` directory when backend unavailable +- **Images**: Try backend API first, fallback to local `pics/` directory -If the backend is unavailable, the frontend falls back to loading `fizika.json` directly. +**Graceful degradation** - Quiz works even when backend is down, using local files. ## 📊 API Endpoints diff --git a/backend/.env.example b/backend/.env.example index 64159d5..ce7079c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -3,4 +3,8 @@ PORT=3001 NODE_ENV=development # CORS Configuration -FRONTEND_URL=* \ No newline at end of file +FRONTEND_URL=* + +# File Paths +DATA_PATH=../fizika.json +PICS_PATH=../pics \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 6d67a14..cf413b1 100644 --- a/backend/README.md +++ b/backend/README.md @@ -56,8 +56,8 @@ A secure Node.js/Express backend for managing physics quiz questions and images ``` 4. **Access the admin interface:** - - Open http://localhost:3001/admin.html - - Default password: `admin123` (change this!) + - Open http://localhost:3001/ + - No authentication required ### Local Development @@ -169,7 +169,7 @@ The project includes a GitHub Actions workflow that: ## Admin Interface -Access the admin interface at `/admin.html`: +Access the admin interface at `/`: - **Questions Tab**: Add, edit, delete quiz questions - **Images Tab**: Upload and manage image files - **Responsive Design**: Works on desktop and mobile diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 25feaa4..a72505e 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -11,6 +11,8 @@ services: - NODE_ENV=production - PORT=3001 - FRONTEND_URL=${FRONTEND_URL:-*} + - DATA_PATH=${DATA_PATH:-/usr/src/app/fizika.json} + - PICS_PATH=${PICS_PATH:-/usr/src/app/pics} volumes: # Mount data files - ./fizika.json:/usr/src/app/fizika.json:ro @@ -37,6 +39,8 @@ services: - NODE_ENV=development - PORT=3001 - FRONTEND_URL=* + - DATA_PATH=/usr/src/app/fizika.json + - PICS_PATH=/usr/src/app/pics volumes: - ./backend:/usr/src/app - ./fizika.json:/usr/src/app/fizika.json diff --git a/backend/server.js b/backend/server.js index 2cef01d..d900778 100644 --- a/backend/server.js +++ b/backend/server.js @@ -15,16 +15,25 @@ app.use(cors({ credentials: true })); -app.use(express.json({ limit: '10mb' })); +app.use(express.json({ limit: '100mb' })); app.use(express.static('public')); // File paths -const DATA_PATH = path.join(__dirname, '../frontend/fizika.json'); -const PICS_PATH = path.join(__dirname, '../frontend/pics'); +const DATA_PATH = process.env.DATA_PATH || path.join(__dirname, '../fizika.json'); +const PICS_PATH = process.env.PICS_PATH || path.join(__dirname, '../pics'); // Multer configuration for image uploads +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, PICS_PATH); + }, + filename: (req, file, cb) => { + cb(null, file.originalname); + } +}); + const upload = multer({ - dest: PICS_PATH, + storage: storage, limits: { fileSize: 5 * 1024 * 1024 }, // 5MB fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { @@ -32,9 +41,6 @@ const upload = multer({ } else { cb(new Error('Only images allowed')); } - }, - filename: (req, file, cb) => { - cb(null, file.originalname); } }); diff --git a/frontend/js/fizika.js b/frontend/js/fizika.js index 42c481b..d800d34 100644 --- a/frontend/js/fizika.js +++ b/frontend/js/fizika.js @@ -25,68 +25,91 @@ async function ajaxLoad(type) { $("#loadingGif").show(); let result = ""; - if (type == 1) { - var source = - "^" + $("#evszam").val() + $("#honap").val() + $("#feladat").val() + "$"; - for (var i = 0; i <= 3; i++) { - source = source.replace("all", ".*"); - console.log(source); + + try { + if (type == 1) { + var source = + "^" + $("#evszam").val() + $("#honap").val() + $("#feladat").val() + "$"; + for (var i = 0; i <= 3; i++) { + source = source.replace("all", ".*"); + console.log(source); + } + result = await loadQuestions(true, undefined, source, 1000000); + } else if (type == 2) { + result = await loadQuestions( + false, + [ + "mk", + "md", + "me", + "mf", + "mr", + "h", + "es", + "ee", + "ev", + "m", + "o", + "ah", + "am", + "cs", + "v", + ], + undefined, + 15 + ); + } else { + var NOQ = $("#numberof").val() ? $("#numberof").val() : 15; + categories = [ + $("#mk").prop("checked") ? "mk" : "", + $("#md").prop("checked") ? "md" : "", + $("#me").prop("checked") ? "me" : "", + $("#mf").prop("checked") ? "mf" : "", + $("#mr").prop("checked") ? "mr" : "", + $("#h").prop("checked") ? "h" : "", + $("#es").prop("checked") ? "es" : "", + $("#ee").prop("checked") ? "ee" : "", + $("#ev").prop("checked") ? "ev" : "", + $("#m").prop("checked") ? "m" : "", + $("#o").prop("checked") ? "o" : "", + $("#ah").prop("checked") ? "ah" : "", + $("#am").prop("checked") ? "am" : "", + $("#cs").prop("checked") ? "cs" : "", + $("#v").prop("checked") ? "v" : "", + ]; + result = await loadQuestions(false, categories, undefined, NOQ); } - result = await loadQuestions(true, undefined, source, 1000000); - } else if (type == 2) { - result = await loadQuestions( - false, - [ - "mk", - "md", - "me", - "mf", - "mr", - "h", - "es", - "ee", - "ev", - "m", - "o", - "ah", - "am", - "cs", - "v", - ], - undefined, - 15 - ); - } else { - var NOQ = $("#numberof").val() ? $("#numberof").val() : 15; - categories = [ - $("#mk").prop("checked") ? "mk" : "", - $("#md").prop("checked") ? "md" : "", - $("#me").prop("checked") ? "me" : "", - $("#mf").prop("checked") ? "mf" : "", - $("#mr").prop("checked") ? "mr" : "", - $("#h").prop("checked") ? "h" : "", - $("#es").prop("checked") ? "es" : "", - $("#ee").prop("checked") ? "ee" : "", - $("#ev").prop("checked") ? "ev" : "", - $("#m").prop("checked") ? "m" : "", - $("#o").prop("checked") ? "o" : "", - $("#ah").prop("checked") ? "ah" : "", - $("#am").prop("checked") ? "am" : "", - $("#cs").prop("checked") ? "cs" : "", - $("#v").prop("checked") ? "v" : "", - ]; - result = await loadQuestions(false, categories, undefined, NOQ); - } - $("#loadingGif").hide(); - $("#content").html(result); - $("#state2").hide(); - if ( - result != - '
Nem található a keresésnek megfelelő feladat!
' - ) { - $("#megoldas").show(); - $("#state").html("Feladatok sikeresen letöltve!"); + $("#loadingGif").hide(); + $("#content").html(result); + $("#state2").hide(); + if ( + result != + '
Nem található a keresésnek megfelelő feladat!
' + ) { + $("#megoldas").show(); + $("#state").html("Feladatok sikeresen letöltve!"); + } + } catch (error) { + $("#loadingGif").hide(); + $("#content").html(` +
+ + Nem sikerült betölteni a feladatokat + +

+ ${error.message} +

+

+ Ellenőrizd az internetkapcsolatot vagy próbáld újra. +

+ +
+ `); + $("#state").html("Hiba a feladatok betöltésekor"); + console.error('Quiz loading error:', error); } } diff --git a/frontend/js/load.js b/frontend/js/load.js index a068f4b..6c10b64 100644 --- a/frontend/js/load.js +++ b/frontend/js/load.js @@ -1,5 +1,20 @@ let questions = null; -const API_BASE = window.location.hostname === 'localhost' ? 'http://localhost:3001' : 'https://your-backend-domain.com'; + +// Auto-detect API base URL +const getApiBase = () => { + const protocol = window.location.protocol; + const hostname = window.location.hostname; + + // If running on localhost, assume backend is on port 3001 + if (hostname === 'localhost' || hostname === '127.0.0.1') { + return `${protocol}//${hostname}:3001`; + } + + // For production, assume backend is on same origin + return "https://fizika-backend.schmelczer.dev" +}; + +const API_BASE = getApiBase(); const loadQuestions = async ( isSearch, @@ -9,10 +24,25 @@ const loadQuestions = async ( ) => { if (questions === null) { try { - questions = await (await fetch(`${API_BASE}/api/fizika`)).json(); + const response = await fetch(`${API_BASE}/api/fizika`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + questions = await response.json(); + console.log('Questions loaded from backend API'); } catch (error) { - console.error('Failed to load questions from API, falling back to local file:', error); - questions = await (await fetch("fizika.json")).json(); + console.warn('Failed to load questions from API, falling back to local file:', error); + try { + const fallbackResponse = await fetch("fizika.json"); + if (!fallbackResponse.ok) { + throw new Error(`Local file not available: ${fallbackResponse.status}`); + } + questions = await fallbackResponse.json(); + console.log('Questions loaded from local fallback file'); + } catch (fallbackError) { + console.error('Both API and local file failed:', fallbackError); + throw new Error('Unable to load quiz data from either backend API or local file'); + } } } @@ -38,7 +68,7 @@ const loadQuestions = async (

${i + 1}.

${source}

${description}
- ${image ? `
` : ""} + ${image ? `
` : ""}
@@ -49,14 +79,13 @@ const loadQuestions = async (
- ${ - d - ? ` + ${d + ? `
` - : "" - } + : "" + }