0b34be8449
gates / gates (push) Successful in 8m50s
Staged under /preview/m3/. Nothing live. 19 shell pages in total with the home.
Each page keeps its own <head> byte-for-byte apart from two additions (shell.css
and the no-js/js hook), so title, meta description, canonical, hreflang, Open Graph
and JSON-LD all survive the move untouched. The body loses the website's
<nav>/<footer> and the old hamburger script, and gains the shell.
PROVED, not assumed - a word-level diff of every page's visible text, live vs
shell, with the nav and footer removed from both:
* 18 of 18 pages: visible text IDENTICAL after the one allowed substitution
* adatkezeles and feltetelek: identical with NO substitution permitted at all.
The legal texts are reviewed documents; the frame changed, the words did not.
That check earned itself immediately. The first version of the page-header
transform rebuilt the block from its <h1> and first <p> and SILENTLY DROPPED the
rest: the "Fontos tudnivalok" / "Good to know" disclaimer banner on both apps
pages and the 404's English line - 168 words across three pages. Nothing looked
wrong on the page. The diff found it.
Em dashes: 318 replaced with the Hungarian gondolatjel across 16 pages (gyik and
en/faq 104 each). The FAQ pages' JSON-LD is swept with the SAME substitution, so
gate 17's equality with the visible answers still holds - it does, on both.
scripts/site_gates.py:
* SHELL now holds all 19 pages.
* is_en(): a page is English if it is a TWINS value OR lives under an en/
directory. Without the second half the dash-language, <html lang> and
no-Hungarian gates all judged preview/m3/en/* as Hungarian - 30 false fails.
* norm_block() also drops `is-open` and `aria-expanded`, because the shell
renders the nav group containing the active page open, exactly as the
controller does, and that legitimately differs per page.
The last change LOOSENS a check, so it was red-proofed BOTH ways: a one-character
difference in one menu label still convicts, and a state-only difference (the
active marker moving to another item) correctly does not.
One generator now owns the shell. The home page's content comes from m3.py and its
shell from the same function every other page uses, so the two cannot drift - which
they already had, and gate 3b caught it.
656 lines
35 KiB
HTML
656 lines
35 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="hu" class="no-js">
|
||
<head>
|
||
<script>document.documentElement.className=document.documentElement.className.replace('no-js','js');</script>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Kapcsolat — Kérj személyre szabott ajánlatot otthoni szerveredhez | Felhom.eu</title>
|
||
<meta name="description" content="Írj nekünk és személyre szabott ajánlatot készítünk otthoni szerveredhez. Kérdésed van? Segítünk! Email: info@felhom.eu">
|
||
|
||
<meta name="keywords" content="kapcsolat, ajánlatkérés, otthoni szerver ajánlat, Felhom elérhetőség">
|
||
<meta name="robots" content="index, follow">
|
||
<link rel="canonical" href="https://felhom.eu/kapcsolat">
|
||
<link rel="alternate" hreflang="hu" href="https://felhom.eu/kapcsolat">
|
||
<link rel="alternate" hreflang="en" href="https://felhom.eu/en/contact">
|
||
<link rel="alternate" hreflang="x-default" href="https://felhom.eu/kapcsolat">
|
||
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:url" content="https://felhom.eu/kapcsolat">
|
||
<meta property="og:title" content="Kapcsolat — Felhom.eu">
|
||
<meta property="og:description" content="Írj nekünk és személyre szabott ajánlatot készítünk otthoni szerveredhez.">
|
||
<meta property="og:image" content="https://felhom.eu/assets/og-image.png">
|
||
<meta property="og:locale" content="hu_HU">
|
||
<meta property="og:site_name" content="Felhom.eu">
|
||
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="Kapcsolat — Felhom.eu">
|
||
<meta name="twitter:description" content="Kérdésed van? Írj nekünk: info@felhom.eu">
|
||
<meta name="twitter:image" content="https://felhom.eu/assets/og-image.png">
|
||
<!-- shared assets: bump ?v= together on any site.css/icons.svg change -->
|
||
<link rel="stylesheet" href="/assets/site.css?v=19">
|
||
<link rel="stylesheet" href="/assets/shell.css?v=19">
|
||
<link rel="icon" type="image/png" href="/assets/logo_favicon_2.svg">
|
||
<script defer src="https://stats.felhom.eu/script.js" data-website-id="d419db57-5e1c-48df-bdb3-c29f25fb1661"></script>
|
||
</head>
|
||
<body class="shell page-kapcsolat">
|
||
<header class="mobile-topbar">
|
||
<a href="/preview/m3/" class="mobile-topbar-logo"><img src="/assets/logo_notext_white.svg" alt="Felhom.eu"></a>
|
||
<button type="button" class="nav-burger" id="navBurger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
|
||
<span class="hamburger-box"><span class="hamburger-line"></span><span class="hamburger-line"></span><span class="hamburger-line"></span></span>
|
||
</button>
|
||
</header>
|
||
<div class="nav-backdrop" id="navBackdrop" hidden></div>
|
||
<nav class="sidebar" id="sidebar">
|
||
<div class="sidebar-header">
|
||
<a href="/preview/m3/" class="sidebar-logo-link">
|
||
<img src="/assets/logo_notext_white.svg" alt="" class="sidebar-logo" aria-hidden="true">
|
||
<img src="/assets/logo-wordmark.png" alt="felhom.eu" class="sidebar-word">
|
||
</a>
|
||
</div>
|
||
<ul class="nav-links">
|
||
<li><a href="/preview/m3/"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-home"/></svg>Kezdőlap</a></li>
|
||
<li><a href="/preview/m3/#szolgaltatasok"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-rocket"/></svg>Szolgáltatások</a></li>
|
||
<li class="nav-group">
|
||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-apps"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg>Alkalmazások<svg class="ico nav-chevron" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chevron-down"/></svg></button>
|
||
<ul id="nav-group-apps" class="nav-links nav-links-sub nav-links-nested">
|
||
<li><a href="/preview/m3/alkalmazasok">Minden alkalmazás</a></li>
|
||
<li><a href="/preview/m3/alkalmazasok#fajlkezeles-irodai">Fájlkezelés & Irodai</a></li>
|
||
<li><a href="/preview/m3/alkalmazasok#media-szorakozas">Média & Szórakozás</a></li>
|
||
<li><a href="/preview/m3/alkalmazasok#otthon-eletmod">Otthon & Életmód</a></li>
|
||
<li><a href="/preview/m3/alkalmazasok#produktivitas-tudaskezeles">Produktivitás & Tudáskezelés</a></li>
|
||
<li><a href="/preview/m3/alkalmazasok#rendszer-fejlesztes">Rendszer & Fejlesztés</a></li>
|
||
</ul>
|
||
</li>
|
||
<li><a href="/preview/m3/technologiak"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-layers"/></svg>Technológiák</a></li>
|
||
<li><a href="/preview/m3/biztonsagimentes"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-shield-check"/></svg>Biztonsági mentés</a></li>
|
||
</ul>
|
||
<div class="sidebar-bottom">
|
||
<a href="/preview/m3/teszteles" class="btn btn-outline sidebar-cta">Jelentkezem tesztelőnek →</a>
|
||
<div class="nav-group-label">A Felhomról</div>
|
||
<ul class="nav-links nav-links-sub">
|
||
<li><a href="/preview/m3/#hogyan-mukodik">Hogyan működik</a></li>
|
||
<li><a href="/preview/m3/gyik">GYIK</a></li>
|
||
<li><a href="/preview/m3/kapcsolat" class="active">Kapcsolat</a></li>
|
||
</ul>
|
||
<div class="sidebar-footer">
|
||
<details class="lang-globe"><summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-globe"/></svg></summary><ul class="lang-globe-menu"><li><a href="/preview/m3/kapcsolat" lang="hu" hreflang="hu" class="lang-globe-item current" aria-current="true">Magyar</a></li><li><a href="/preview/m3/en/contact" lang="en" hreflang="en" class="lang-globe-item">English</a></li></ul></details>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
|
||
<div class="page-header">
|
||
<h1>Írj nekünk <span>bátran!</span></h1>
|
||
</div>
|
||
<p class="page-lede">Kérdésed van, vagy szeretnéd megbeszélni az igényeidet? Töltsd ki az űrlapot, és hamarosan válaszolunk.</p>
|
||
|
||
|
||
<main class="content">
|
||
<section class="content-section" style="padding-top: 0;">
|
||
<div class="container">
|
||
<div class="contact-layout">
|
||
|
||
<!-- Form -->
|
||
<div class="contact-form-card">
|
||
<div id="formStatus" class="form-status"></div>
|
||
|
||
<form id="contactForm" novalidate>
|
||
<div style="position:absolute;left:-9999px;" aria-hidden="true">
|
||
<input type="text" name="website" tabindex="-1" autocomplete="off">
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="name">Név <span class="required">*</span></label>
|
||
<input type="text" id="name" name="name" placeholder="Teljes neved" required>
|
||
<div class="error-message" id="nameError">Kérlek, add meg a neved.</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="email">Email <span class="required">*</span></label>
|
||
<input type="email" id="email" name="email" placeholder="pelda@email.hu" required>
|
||
<div class="error-message" id="emailError">Kérlek, adj meg egy érvényes email címet.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="subject">Tárgy <span class="required">*</span></label>
|
||
<select id="subject" name="subject" required>
|
||
<option value="" disabled selected>Válassz témát...</option>
|
||
<option value="zart-teszt">Zárt teszt – jelentkezem tesztelőnek</option>
|
||
<option value="erdeklodes">Érdeklődés – szeretném megismerni a szolgáltatást</option>
|
||
<option value="arajanlat">Árajánlat kérés – konkrét igényeim vannak</option>
|
||
<option value="tamogatas">Támogatás – meglévő szerveremet érintő kérdés</option>
|
||
<option value="egyeb">Egyéb</option>
|
||
</select>
|
||
<div class="error-message" id="subjectError">Kérlek, válassz egy témát.</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="message">Üzenet <span class="required">*</span></label>
|
||
<textarea id="message" name="message" placeholder="Írd le, miben segíthetünk..." required></textarea>
|
||
<div class="error-message" id="messageError">Kérlek, írd le az üzeneted.</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Fájl csatolás <span style="color: var(--text-3); font-weight: 400;">(opcionális)</span></label>
|
||
<div class="file-upload-zone" id="dropZone">
|
||
<input type="file" id="fileInput" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.gif,.webp,.txt,.csv,.zip,.7z">
|
||
<div class="upload-icon"><svg class="ico-tile" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-paperclip"/></svg></div>
|
||
<div class="upload-text">
|
||
<strong>Kattints ide</strong> vagy húzd ide a fájlokat
|
||
</div>
|
||
<div class="upload-hint">PDF, képek, dokumentumok · Max. 20 MB összesen</div>
|
||
</div>
|
||
<div class="file-list" id="fileList"></div>
|
||
<div class="file-total-size" id="fileTotalSize"></div>
|
||
</div>
|
||
|
||
<div class="checkbox-group">
|
||
<input type="checkbox" id="privacy" name="privacy" required>
|
||
<label for="privacy">
|
||
Elolvastam az <a href="/adatkezeles" target="_blank" rel="noopener">Adatkezelési tájékoztatót</a>, és hozzájárulok, hogy a megadott nevemet, e-mail-címemet, üzenetemet és csatolt fájljaimat a megkeresésem megválaszolására kezeljék. Az üzenetet levélküldő és levelezési szolgáltatók továbbítják. A hozzájárulásomat bármikor visszavonhatom az info@felhom.eu címen.
|
||
</label>
|
||
</div>
|
||
<div class="error-message" id="privacyError" style="margin-top: -16px; margin-bottom: 16px;">Az adatkezelési hozzájárulás szükséges.</div>
|
||
|
||
<button type="submit" class="submit-button" id="submitBtn">
|
||
<span class="btn-text">Üzenet küldése →</span>
|
||
<span class="spinner"></span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Sidebar -->
|
||
<div class="contact-sidebar">
|
||
<div class="sidebar-card">
|
||
<h3><span class="icon"><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-mail"/></svg></span> Közvetlen email</h3>
|
||
<p>Ha inkább emailben írnál:</p>
|
||
<a href="mailto:info@felhom.eu" class="direct-email">
|
||
<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-mail"/></svg> info@felhom.eu
|
||
</a>
|
||
</div>
|
||
|
||
<div class="sidebar-card">
|
||
<h3><span class="icon"><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-clock"/></svg></span> Válaszidő</h3>
|
||
<p>Általában <strong style="color: var(--blue-bright);">24 órán belül</strong> válaszolunk munkanapokon.</p>
|
||
<p>Sürgős támogatási kérdés esetén jelezd a tárgyban.</p>
|
||
</div>
|
||
|
||
<div class="sidebar-card">
|
||
<h3><span class="icon"><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-lightbulb"/></svg></span> Miben segíthetünk?</h3>
|
||
<ul class="sidebar-list">
|
||
<li><span class="li-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-monitor"/></svg></span> Szerver konfiguráció tanácsadás</li>
|
||
<li><span class="li-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg></span> Alkalmazás telepítés és beállítás</li>
|
||
<li><span class="li-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-lock"/></svg></span> Biztonsági mentés kialakítás</li>
|
||
<li><span class="li-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-globe"/></svg></span> Távoli elérés beállítása</li>
|
||
<li><span class="li-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-wrench"/></svg></span> Meglévő szerver karbantartás</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="content-section alt-bg" id="ki-all-mogotte">
|
||
<div class="container">
|
||
<div class="who">
|
||
<figure class="who-photo">
|
||
<img src="/assets/operator-portrait.webp" alt="Nagyfenyvesi Viktor, a Felhom üzemeltetője" width="720" height="900" loading="lazy">
|
||
</figure>
|
||
<div class="who-text">
|
||
<h2>Ki áll <span>mögötte</span></h2>
|
||
<p><strong>Nagyfenyvesi Viktor vagyok.</strong> Tíz éve tervezek hálózatokat és infrastruktúrát, ma DevOps mérnökként dolgozom, otthon pedig évek óta megy a saját szerverem; a mostani nagyjából egy éve fut.</p>
|
||
<p>A Felhom onnan indult, hogy ilyen szolgáltatást itthon nem találtam. Saját szervert sokan meg tudnak építeni, de utána menteni, frissíteni és figyelni is kell valakinek. Ezt a részét vállalom át.</p>
|
||
<p>Ha írsz, én válaszolok, és nem ígérek olyat, amit a doboz ma nem tud.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<script>
|
||
(() => {
|
||
'use strict';
|
||
|
||
// ==============================================
|
||
// CONFIGURATION
|
||
// ==============================================
|
||
const CONFIG = {
|
||
// TODO: Replace with your actual form backend endpoint.
|
||
// Options:
|
||
// 1. Self-hosted: Deploy a small container (e.g. Go/Python) that receives POST and sends via SMTP
|
||
// 2. Formspree: 'https://formspree.io/f/YOUR_FORM_ID'
|
||
// 3. Cloudflare Workers: Deploy a worker that forwards to email
|
||
//
|
||
// For now, falls back to mailto: if no endpoint is configured.
|
||
formEndpoint: '/api/contact',
|
||
|
||
maxTotalFileSize: 20 * 1024 * 1024, // 20 MB
|
||
maxSingleFileSize: 10 * 1024 * 1024, // 10 MB per file
|
||
maxFiles: 5,
|
||
allowedExtensions: ['pdf','doc','docx','xls','xlsx','png','jpg','jpeg','gif','webp','txt','csv','zip','7z'],
|
||
};
|
||
|
||
// ==============================================
|
||
// DOM REFS
|
||
// ==============================================
|
||
const form = document.getElementById('contactForm');
|
||
const submitBtn = document.getElementById('submitBtn');
|
||
const formStatus = document.getElementById('formStatus');
|
||
const dropZone = document.getElementById('dropZone');
|
||
const fileInput = document.getElementById('fileInput');
|
||
const fileListEl = document.getElementById('fileList');
|
||
const fileTotalSizeEl = document.getElementById('fileTotalSize');
|
||
|
||
let attachedFiles = [];
|
||
|
||
// Preselect the subject from ?tema= (the „Zárt teszt" CTA on the landing page
|
||
// links here as /kapcsolat?tema=zart-teszt). Unknown values are ignored.
|
||
(function preselectSubject() {
|
||
const wanted = new URLSearchParams(window.location.search).get('tema');
|
||
if (!wanted) return;
|
||
const select = document.getElementById('subject');
|
||
if (!select) return;
|
||
const match = Array.from(select.options).some(o => o.value === wanted);
|
||
if (match) {
|
||
select.value = wanted;
|
||
} else {
|
||
console.warn('[kapcsolat] unknown ?tema= value, ignoring:', wanted);
|
||
}
|
||
})();
|
||
|
||
// ==============================================
|
||
// VALIDATION
|
||
// ==============================================
|
||
function validateField(field) {
|
||
const errorEl = document.getElementById(field.id + 'Error');
|
||
let valid = true;
|
||
|
||
if (field.type === 'checkbox') {
|
||
valid = field.checked;
|
||
} else if (field.required && !field.value.trim()) {
|
||
valid = false;
|
||
} else if (field.type === 'email' && field.value.trim()) {
|
||
valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(field.value.trim());
|
||
}
|
||
|
||
if (valid) {
|
||
field.classList.remove('error');
|
||
if (errorEl) errorEl.classList.remove('visible');
|
||
} else {
|
||
field.classList.add('error');
|
||
if (errorEl) errorEl.classList.add('visible');
|
||
}
|
||
|
||
return valid;
|
||
}
|
||
|
||
function validateForm() {
|
||
const fields = ['name', 'email', 'subject', 'message', 'privacy'];
|
||
let allValid = true;
|
||
|
||
fields.forEach(id => {
|
||
const field = document.getElementById(id);
|
||
if (!validateField(field)) allValid = false;
|
||
});
|
||
|
||
return allValid;
|
||
}
|
||
|
||
// Live validation on blur
|
||
['name', 'email', 'subject', 'message', 'privacy'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
const event = el.type === 'checkbox' ? 'change' : 'blur';
|
||
el.addEventListener(event, () => validateField(el));
|
||
// Clear error on input
|
||
el.addEventListener('input', () => {
|
||
if (el.classList.contains('error')) validateField(el);
|
||
});
|
||
});
|
||
|
||
// ==============================================
|
||
// FILE HANDLING
|
||
// ==============================================
|
||
function formatFileSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||
}
|
||
|
||
function getFileIcon(name) {
|
||
const ext = name.split('.').pop().toLowerCase();
|
||
const icons = {
|
||
pdf: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-file-text"/></svg>', doc: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-file-text"/></svg>', docx: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-file-text"/></svg>', xls: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chart-column"/></svg>', xlsx: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chart-column"/></svg>',
|
||
png: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-image"/></svg>', jpg: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-image"/></svg>', jpeg: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-image"/></svg>', gif: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-image"/></svg>', webp: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-image"/></svg>',
|
||
txt: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-file-text"/></svg>', csv: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chart-column"/></svg>', zip: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg>', '7z': '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg>',
|
||
};
|
||
return icons[ext] || '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-paperclip"/></svg>';
|
||
}
|
||
|
||
function getTotalFileSize() {
|
||
return attachedFiles.reduce((sum, f) => sum + f.size, 0);
|
||
}
|
||
|
||
function addFiles(fileArray) {
|
||
for (const file of fileArray) {
|
||
// Check extension
|
||
const ext = file.name.split('.').pop().toLowerCase();
|
||
if (!CONFIG.allowedExtensions.includes(ext)) {
|
||
showStatus(`A "${file.name}" fájltípus nem engedélyezett.`, 'error-state');
|
||
continue;
|
||
}
|
||
|
||
// Check single file size
|
||
if (file.size > CONFIG.maxSingleFileSize) {
|
||
showStatus(`A "${file.name}" túl nagy (max. ${formatFileSize(CONFIG.maxSingleFileSize)} / fájl).`, 'error-state');
|
||
continue;
|
||
}
|
||
|
||
// Check max file count
|
||
if (attachedFiles.length >= CONFIG.maxFiles) {
|
||
showStatus(`Maximum ${CONFIG.maxFiles} fájl csatolható.`, 'error-state');
|
||
break;
|
||
}
|
||
|
||
// Check total size
|
||
if (getTotalFileSize() + file.size > CONFIG.maxTotalFileSize) {
|
||
showStatus(`Összesített fájlméret meghaladja a ${formatFileSize(CONFIG.maxTotalFileSize)} limitet.`, 'error-state');
|
||
break;
|
||
}
|
||
|
||
// Check duplicate
|
||
if (attachedFiles.some(f => f.name === file.name && f.size === file.size)) {
|
||
continue;
|
||
}
|
||
|
||
attachedFiles.push(file);
|
||
}
|
||
|
||
renderFileList();
|
||
}
|
||
|
||
function removeFile(index) {
|
||
attachedFiles.splice(index, 1);
|
||
renderFileList();
|
||
// Reset file input so same file can be re-added
|
||
fileInput.value = '';
|
||
}
|
||
|
||
function renderFileList() {
|
||
if (attachedFiles.length === 0) {
|
||
fileListEl.innerHTML = '';
|
||
fileTotalSizeEl.textContent = '';
|
||
return;
|
||
}
|
||
|
||
fileListEl.innerHTML = attachedFiles.map((file, i) => `
|
||
<div class="file-item">
|
||
<span class="file-icon">${getFileIcon(file.name)}</span>
|
||
<div class="file-info">
|
||
<div class="file-name" title="${file.name}">${file.name}</div>
|
||
<div class="file-size">${formatFileSize(file.size)}</div>
|
||
</div>
|
||
<button type="button" class="file-remove" data-index="${i}" title="Eltávolítás">×</button>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Bind remove buttons
|
||
fileListEl.querySelectorAll('.file-remove').forEach(btn => {
|
||
btn.addEventListener('click', () => removeFile(parseInt(btn.dataset.index)));
|
||
});
|
||
|
||
// Total size indicator
|
||
const total = getTotalFileSize();
|
||
const pct = (total / CONFIG.maxTotalFileSize) * 100;
|
||
fileTotalSizeEl.textContent = `${formatFileSize(total)} / ${formatFileSize(CONFIG.maxTotalFileSize)}`;
|
||
fileTotalSizeEl.className = 'file-total-size';
|
||
if (pct > 90) fileTotalSizeEl.classList.add('error-text');
|
||
else if (pct > 70) fileTotalSizeEl.classList.add('warning');
|
||
}
|
||
|
||
// File input change
|
||
fileInput.addEventListener('change', () => {
|
||
if (fileInput.files.length) {
|
||
addFiles(Array.from(fileInput.files));
|
||
fileInput.value = '';
|
||
}
|
||
});
|
||
|
||
// Drag & drop
|
||
['dragenter', 'dragover'].forEach(evt => {
|
||
dropZone.addEventListener(evt, e => {
|
||
e.preventDefault();
|
||
dropZone.classList.add('dragover');
|
||
});
|
||
});
|
||
|
||
['dragleave', 'drop'].forEach(evt => {
|
||
dropZone.addEventListener(evt, e => {
|
||
e.preventDefault();
|
||
dropZone.classList.remove('dragover');
|
||
});
|
||
});
|
||
|
||
dropZone.addEventListener('drop', e => {
|
||
if (e.dataTransfer.files.length) {
|
||
addFiles(Array.from(e.dataTransfer.files));
|
||
}
|
||
});
|
||
|
||
// ==============================================
|
||
// STATUS MESSAGES
|
||
// ==============================================
|
||
function showStatus(message, type) {
|
||
formStatus.textContent = message;
|
||
formStatus.className = 'form-status ' + type;
|
||
formStatus.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
|
||
if (type === 'error-state') {
|
||
setTimeout(() => {
|
||
formStatus.className = 'form-status';
|
||
}, 6000);
|
||
}
|
||
}
|
||
|
||
// ==============================================
|
||
// FORM SUBMISSION
|
||
// ==============================================
|
||
form.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
formStatus.className = 'form-status';
|
||
|
||
if (!validateForm()) {
|
||
// Build specific error message
|
||
const missing = [];
|
||
if (!document.getElementById('name').value.trim()) missing.push('név');
|
||
const emailVal = document.getElementById('email').value.trim();
|
||
if (!emailVal || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal)) missing.push('email');
|
||
if (!document.getElementById('subject').value) missing.push('tárgy');
|
||
if (!document.getElementById('message').value.trim()) missing.push('üzenet');
|
||
if (!document.getElementById('privacy').checked) missing.push('adatkezelési hozzájárulás');
|
||
|
||
if (missing.length === 1 && missing[0] === 'adatkezelési hozzájárulás') {
|
||
showStatus('Kérlek, fogadd el az adatkezelési hozzájárulást a küldéshez.', 'error-state');
|
||
} else {
|
||
showStatus('Kérlek, töltsd ki az összes kötelező mezőt.', 'error-state');
|
||
}
|
||
return;
|
||
}
|
||
|
||
submitBtn.classList.add('loading');
|
||
submitBtn.disabled = true;
|
||
|
||
const formData = {
|
||
name: document.getElementById('name').value.trim(),
|
||
email: document.getElementById('email').value.trim(),
|
||
subject: document.getElementById('subject').value,
|
||
message: document.getElementById('message').value.trim(),
|
||
};
|
||
|
||
// Subject label mapping
|
||
const subjectLabels = {
|
||
'zart-teszt': 'Jelentkezés – zárt teszt',
|
||
erdeklodes: 'Érdeklődés',
|
||
arajanlat: 'Árajánlat kérés',
|
||
tamogatas: 'Támogatás',
|
||
egyeb: 'Egyéb',
|
||
};
|
||
|
||
try {
|
||
if (CONFIG.formEndpoint) {
|
||
// --- Backend submission ---
|
||
const data = new FormData();
|
||
data.append('name', formData.name);
|
||
data.append('email', formData.email);
|
||
data.append('subject', subjectLabels[formData.subject] || formData.subject);
|
||
data.append('message', formData.message);
|
||
|
||
attachedFiles.forEach(file => {
|
||
data.append('attachments', file);
|
||
});
|
||
|
||
const response = await fetch(CONFIG.formEndpoint, {
|
||
method: 'POST',
|
||
body: data,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Szerver hiba: ${response.status}`);
|
||
}
|
||
|
||
showStatus('Üzeneted sikeresen elküldtük! Hamarosan válaszolunk.', 'success');
|
||
form.reset();
|
||
attachedFiles = [];
|
||
renderFileList();
|
||
|
||
} else {
|
||
// --- Fallback: mailto ---
|
||
const subjectLine = `[Felhom.eu] ${subjectLabels[formData.subject] || formData.subject}`;
|
||
const body = [
|
||
`Név: ${formData.name}`,
|
||
`Email: ${formData.email}`,
|
||
``,
|
||
formData.message,
|
||
].join('\n');
|
||
|
||
const mailtoUrl = `mailto:info@felhom.eu?subject=${encodeURIComponent(subjectLine)}&body=${encodeURIComponent(body)}`;
|
||
window.location.href = mailtoUrl;
|
||
|
||
if (attachedFiles.length > 0) {
|
||
showStatus('Az email kliens megnyílt. A csatolt fájlokat kézzel kell hozzáadnod az emailhez, mert a mailto: link nem támogatja a mellékleteket.', 'error-state');
|
||
} else {
|
||
showStatus('Az email kliens megnyílt az üzeneteddel. Küld el az emailt a kapcsolatfelvételhez!', 'success');
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('Form submission error:', err);
|
||
showStatus(`Hiba történt a küldés során. Kérlek, próbáld újra vagy írj közvetlenül az info@felhom.eu címre.`, 'error-state');
|
||
} finally {
|
||
submitBtn.classList.remove('loading');
|
||
submitBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
// ==============================================
|
||
// DEBUG HELPER
|
||
// ==============================================
|
||
// Uncomment to test form states:
|
||
// window.debugForm = {
|
||
// showSuccess: () => showStatus('Üzeneted sikeresen elküldtük!', 'success'),
|
||
// showError: () => showStatus('Hiba történt.', 'error-state'),
|
||
// getFiles: () => console.log(attachedFiles),
|
||
// getFormData: () => {
|
||
// const fd = new FormData(form);
|
||
// for (const [k,v] of fd.entries()) console.log(k, v);
|
||
// },
|
||
// };
|
||
|
||
})();
|
||
</script>
|
||
<script>
|
||
(function() {
|
||
var hamburger = document.getElementById('hamburger');
|
||
var navLinks = document.querySelector('.nav-links');
|
||
var overlay = document.getElementById('mobileOverlay');
|
||
var body = document.body;
|
||
|
||
if (!hamburger || !navLinks || !overlay) {
|
||
console.warn('[mobile-menu] Missing elements:', {
|
||
hamburger: !!hamburger,
|
||
navLinks: !!navLinks,
|
||
overlay: !!overlay
|
||
});
|
||
return;
|
||
}
|
||
|
||
function openMenu() {
|
||
hamburger.classList.add('is-active');
|
||
navLinks.classList.add('is-open');
|
||
overlay.classList.add('is-visible');
|
||
body.classList.add('menu-open');
|
||
hamburger.setAttribute('aria-expanded', 'true');
|
||
hamburger.setAttribute('aria-label', 'Menü bezárása');
|
||
}
|
||
|
||
function closeMenu() {
|
||
hamburger.classList.remove('is-active');
|
||
navLinks.classList.remove('is-open');
|
||
overlay.classList.remove('is-visible');
|
||
body.classList.remove('menu-open');
|
||
hamburger.setAttribute('aria-expanded', 'false');
|
||
hamburger.setAttribute('aria-label', 'Menü megnyitása');
|
||
}
|
||
|
||
hamburger.addEventListener('click', function() {
|
||
if (navLinks.classList.contains('is-open')) {
|
||
closeMenu();
|
||
} else {
|
||
openMenu();
|
||
}
|
||
});
|
||
|
||
overlay.addEventListener('click', closeMenu);
|
||
|
||
navLinks.querySelectorAll('a').forEach(function(link) {
|
||
link.addEventListener('click', closeMenu);
|
||
});
|
||
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape' && navLinks.classList.contains('is-open')) {
|
||
closeMenu();
|
||
}
|
||
});
|
||
|
||
window.addEventListener('resize', function() {
|
||
if (window.innerWidth > 900 && navLinks.classList.contains('is-open')) {
|
||
closeMenu();
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
<script>
|
||
(function(){
|
||
var b=document.getElementById('navBurger'),s=document.getElementById('sidebar'),k=document.getElementById('navBackdrop');
|
||
if(!b||!s||!k)return;
|
||
function open(){s.classList.add('is-open');k.hidden=false;document.body.classList.add('nav-open');b.setAttribute('aria-expanded','true');}
|
||
function close(){s.classList.remove('is-open');k.hidden=true;document.body.classList.remove('nav-open');b.setAttribute('aria-expanded','false');}
|
||
b.addEventListener('click',function(){s.classList.contains('is-open')?close():open();});
|
||
k.addEventListener('click',close);
|
||
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&s.classList.contains('is-open')){close();b.focus();}});
|
||
s.querySelectorAll('a').forEach(function(l){l.addEventListener('click',close);});
|
||
window.addEventListener('resize',function(){if(window.innerWidth>768)close();});
|
||
})();
|
||
document.addEventListener('click',function(e){
|
||
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
|
||
if(!btn)return;
|
||
var g=btn.closest('.nav-group');if(!g)return;
|
||
var willOpen=!g.classList.contains('is-open');
|
||
document.querySelectorAll('.nav-group.is-open').forEach(function(x){
|
||
x.classList.remove('is-open');
|
||
var t=x.querySelector('.nav-group-toggle');if(t)t.setAttribute('aria-expanded','false');
|
||
});
|
||
if(willOpen){g.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|